Appearance
MockMvc 与 HtmlUnit 集成:让 Web 测试如虎添翼 🚀
前言:为什么需要 MockMvc 与 HtmlUnit 的集成?
在 Spring Boot 的 Web 应用测试中,我们经常面临这样的困境:
- MockMvc 虽然能够测试 Controller 层,但对于复杂的前端交互(如表单提交、JavaScript 执行)显得力不从心
- 传统的集成测试 需要启动完整的 Web 服务器,测试速度慢,资源消耗大
- 前后端交互测试 往往需要手动构造复杂的 HTTP 请求,维护成本高
IMPORTANT
MockMvc 与 HtmlUnit 的集成为我们提供了一个完美的解决方案:既保持了 MockMvc 的轻量级特性,又获得了 HtmlUnit 强大的 HTML 解析和 JavaScript 执行能力。
核心原理:两个世界的完美融合
TIP
这种集成的核心思想是:让 HtmlUnit 以为它在与真实的 Web 服务器通信,实际上所有的 localhost 请求都被悄悄地重定向到了 MockMvc。
环境准备与基础配置
1. 添加依赖
首先,确保项目中包含了 HtmlUnit 的测试依赖:
kotlin
// build.gradle.kts
testImplementation("org.htmlunit:htmlunit")
testImplementation("org.springframework.boot:spring-boot-starter-test")
2. 基础配置示例
kotlin
@SpringBootTest
@AutoConfigureTestDatabase
class MessageControllerHtmlUnitTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var webClient: WebClient
@BeforeEach
fun setup() {
// 创建集成了 MockMvc 的 HtmlUnit WebClient
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
}
kotlin
@SpringBootTest
@AutoConfigureTestDatabase
class AdvancedMessageControllerTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var webClient: WebClient
@BeforeEach
fun setup() {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.contextPath("/api") // 设置上下文路径
.useMockMvcForHosts("example.com", "api.test.com") // 指定使用 MockMvc 的域名
.build()
}
}
NOTE
MockMvcWebClientBuilder.webAppContextSetup(context)
是集成的关键,它告诉 HtmlUnit 如何将请求路由到我们的 Spring 应用上下文中。
实战案例:消息管理系统测试
让我们通过一个完整的消息管理系统来演示 MockMvc 与 HtmlUnit 集成的强大功能。
1. 被测试的 Controller
kotlin
@Controller
@RequestMapping("/messages")
class MessageController {
private val messages = mutableMapOf<Long, Message>()
private var nextId = 1L
@GetMapping("/form")
fun showCreateForm(model: Model): String {
model.addAttribute("message", Message())
return "message-form" // 返回表单页面
}
@PostMapping
fun createMessage(@ModelAttribute message: Message): String {
message.id = nextId++
messages[message.id!!] = message
return "redirect:/messages/${message.id}"
}
@GetMapping("/{id}")
fun showMessage(@PathVariable id: Long, model: Model): String {
val message = messages[id] ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
model.addAttribute("message", message)
return "message-detail"
}
}
data class Message(
var id: Long? = null,
var summary: String = "",
var text: String = ""
)
2. 对应的 HTML 模板
message-form.html 模板内容
html
<!DOCTYPE html>
<html>
<head>
<title>Create Message</title>
</head>
<body>
<form id="messageForm" th:action="@{/messages}" method="post">
<div>
<label for="summary">Summary:</label>
<input type="text" id="summary" name="summary" required/>
</div>
<div>
<label for="text">Message:</label>
<textarea id="text" name="text" required></textarea>
</div>
<input type="submit" value="Create Message"/>
</form>
</body>
</html>
3. 完整的测试用例
kotlin
@SpringBootTest
@AutoConfigureTestDatabase
class MessageControllerHtmlUnitTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var webClient: WebClient
@BeforeEach
fun setup() {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
@Test
fun `应该能够创建新消息并验证结果`() {
// 第一步:获取创建消息的表单页面
val createFormPage: HtmlPage = webClient.getPage("http://localhost/messages/form")
// 第二步:填写表单数据
val form: HtmlForm = createFormPage.getHtmlElementById("messageForm")
val summaryInput: HtmlTextInput = createFormPage.getHtmlElementById("summary")
summaryInput.valueAttribute = "Spring Boot 测试"
val textInput: HtmlTextArea = createFormPage.getHtmlElementById("text")
textInput.text = "MockMvc 与 HtmlUnit 集成让测试变得更加简单!"
// 第三步:提交表单
val submitButton: HtmlSubmitInput = form.getOneHtmlElementByAttribute("input", "type", "submit")
val resultPage: HtmlPage = submitButton.click()
// 第四步:验证结果
assertThat(resultPage.url.toString()).endsWith("/messages/1")
val displayedId = resultPage.getHtmlElementById("id").textContent
assertThat(displayedId).isEqualTo("1")
val displayedSummary = resultPage.getHtmlElementById("summary").textContent
assertThat(displayedSummary).isEqualTo("Spring Boot 测试")
val displayedText = resultPage.getHtmlElementById("text").textContent
assertThat(displayedText).isEqualTo("MockMvc 与 HtmlUnit 集成让测试变得更加简单!")
}
@Test
fun `应该能够处理表单验证错误`() {
val createFormPage: HtmlPage = webClient.getPage("http://localhost/messages/form")
val form: HtmlForm = createFormPage.getHtmlElementById("messageForm")
// 故意不填写必填字段
val submitButton: HtmlSubmitInput = form.getOneHtmlElementByAttribute("input", "type", "submit")
// 在真实场景中,这里会触发客户端验证或服务端验证
val resultPage: HtmlPage = submitButton.click()
// 验证错误处理逻辑
// 具体的验证逻辑取决于你的应用实现
}
}
高级特性:JavaScript 支持与 CDN 测试
JavaScript 执行能力
IMPORTANT
HtmlUnit 使用 Mozilla Rhino 引擎来执行 JavaScript,这意味着我们可以测试包含 JavaScript 交互的页面。
kotlin
@Test
fun `应该能够测试JavaScript交互`() {
// 启用 JavaScript 支持
webClient.options.isJavaScriptEnabled = true
webClient.options.isThrowExceptionOnScriptError = false
val page: HtmlPage = webClient.getPage("http://localhost/interactive-form")
// 触发 JavaScript 事件
val button: HtmlButton = page.getHtmlElementById("dynamicButton")
val resultPage = button.click()
// 等待 JavaScript 执行完成
webClient.waitForBackgroundJavaScript(2000)
// 验证 JavaScript 执行结果
val dynamicContent = resultPage.getHtmlElementById("dynamicContent")
assertThat(dynamicContent.textContent).contains("动态生成的内容")
}
CDN 和外部资源测试
kotlin
@Test
fun `应该能够正确处理CDN资源`() {
// 配置 MockMvc 只处理 localhost,其他请求走真实网络
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.useMockMvcForHosts("localhost") // 只有 localhost 使用 MockMvc
.build()
val page: HtmlPage = webClient.getPage("http://localhost/page-with-cdn")
// 页面中的 CDN 资源(如 jQuery、Bootstrap)会通过真实网络请求加载
// 而应用本身的请求仍然通过 MockMvc 处理
assertThat(page.titleText).isEqualTo("包含CDN资源的页面")
}
与传统 MockMvc 测试的对比
kotlin
@Test
fun `传统MockMvc方式创建消息`() {
// 需要手动构造请求参数
mockMvc.perform(post("/messages")
.param("summary", "Spring Boot 测试")
.param("text", "传统方式需要手动构造参数"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("/messages/*"))
// 无法直接验证重定向后的页面内容
// 需要额外的请求来获取创建后的消息详情
}
kotlin
@Test
fun `HtmlUnit集成方式创建消息`() {
// 像真实用户一样操作页面
val formPage = webClient.getPage("http://localhost/messages/form")
val form = formPage.getHtmlElementById("messageForm")
// 直观地填写表单
formPage.getHtmlElementById<HtmlTextInput>("summary")
.valueAttribute = "Spring Boot 测试"
// 提交表单并自动跟随重定向
val resultPage = form.getOneHtmlElementByAttribute<HtmlSubmitInput>
("input", "type", "submit").click()
// 直接验证最终页面内容
assertThat(resultPage.getHtmlElementById("summary").textContent)
.isEqualTo("Spring Boot 测试")
}
最佳实践与注意事项
1. 性能优化
kotlin
@BeforeEach
fun setup() {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
// 优化 HtmlUnit 性能
webClient.options.apply {
isCssEnabled = false // 禁用 CSS 处理以提高速度
isJavaScriptEnabled = true // 根据需要启用 JS
isThrowExceptionOnFailingStatusCode = false
isThrowExceptionOnScriptError = false
}
}
2. 错误处理
WARNING
在测试中要注意处理可能出现的各种异常情况。
kotlin
@Test
fun `应该优雅处理页面加载失败`() {
assertThrows<FailingHttpStatusCodeException> {
webClient.getPage("http://localhost/non-existent-page")
}
}
3. 资源清理
kotlin
@AfterEach
fun cleanup() {
webClient.close() // 确保释放资源
}
总结
MockMvc 与 HtmlUnit 的集成为我们提供了一个强大而优雅的 Web 应用测试解决方案:
✅ 无需启动真实服务器:保持测试的轻量级和快速执行
✅ 真实的用户交互:像真实用户一样填写表单、点击按钮
✅ JavaScript 支持:能够测试复杂的前端交互逻辑
✅ 灵活的配置:可以精确控制哪些请求使用 MockMvc,哪些走真实网络
✅ 简化的测试代码:相比传统 MockMvc,测试代码更加直观和易维护
TIP
这种集成方式特别适合测试表单提交、页面跳转、前后端交互等场景。它让我们能够在保持单元测试速度的同时,获得接近集成测试的覆盖度。
通过 MockMvc 与 HtmlUnit 的完美结合,我们的 Web 应用测试变得更加全面、可靠和易于维护! 🎉