Appearance
MockMvc 与 WebDriver 集成:让 Web 测试更加优雅 🚀
引言:为什么需要 WebDriver?
在前面的章节中,我们已经学会了如何使用 MockMvc 结合原生的 HtmlUnit API 进行 Web 测试。但是,当测试场景变得复杂时,我们会发现代码变得冗长且难以维护。这时候,Selenium WebDriver 就像一位优雅的舞者,为我们提供了更加简洁和强大的 API。
NOTE
尽管 WebDriver 是 Selenium 的一部分,但它并不需要 Selenium Server 来运行测试。
核心问题:传统测试方式的痛点
问题场景分析
假设我们需要测试一个消息创建功能,这个测试涉及:
- 查找 HTML 表单输入元素
- 填写表单数据
- 进行各种断言验证
- 测试错误条件处理
使用传统的 HtmlUnit 方式,我们可能会写出这样的代码:
kotlin
// 在多个测试中重复出现的代码
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
kotlin
fun createMessage(currentPage: HtmlPage, summary: String, text: String): HtmlPage {
setSummary(currentPage, summary)
// ...
}
fun setSummary(currentPage: HtmlPage, summary: String) {
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
}
进一步优化:Page Object 模式
我们可以将逻辑封装到一个代表当前页面的对象中:
kotlin
class CreateMessagePage(private val currentPage: HtmlPage) {
val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")
val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")
fun <T> createMessage(summary: String, text: String): T {
setSummary(summary)
val result = submit.click()
val error = at(result)
return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
}
fun setSummary(summary: String) {
summaryInput.setValueAttribute(summary)
}
fun at(page: HtmlPage): Boolean {
return "Create Message" == page.titleText
}
}
TIP
这种模式被称为 Page Object Pattern。虽然我们可以在 HtmlUnit 中实现这个模式,但 WebDriver 提供了更强大的工具来简化实现。
MockMvc 与 WebDriver 的完美结合
环境配置
首先,确保项目包含测试依赖:
xml
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>htmlunit3-driver</artifactId>
<scope>test</scope>
</dependency>
基础设置
使用 MockMvcHtmlUnitDriverBuilder
创建集成了 MockMvc 的 Selenium WebDriver:
kotlin
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
IMPORTANT
这个配置确保所有指向 localhost
的 URL 都会被定向到我们的 MockMvc 实例,而无需真实的 HTTP 连接。其他 URL 则通过正常的网络连接请求,这让我们可以轻松测试 CDN 的使用。
实际应用:优雅的测试编写
页面导航
kotlin
val page = CreateMessagePage.to(driver)
表单操作与提交
kotlin
val viewMessagePage = page.createMessage(
ViewMessagePage::class,
expectedSummary,
expectedText
)
强大的 Page Object 实现
让我们看看使用 WebDriver 的 CreateMessagePage
实现有多么简洁:
kotlin
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) {
// WebDriver 的 PageFactory 自动解析 WebElement
// 根据字段名查找页面中对应 id 或 name 的元素
private lateinit var summary: WebElement
private lateinit var text: WebElement
// 使用 @FindBy 注解自定义查找行为
@FindBy(css = "input[type=submit]")
private lateinit var submit: WebElement
fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
this.summary.sendKeys(summary)
text.sendKeys(details)
submit.click()
return PageFactory.initElements(driver, resultPage)
}
companion object {
fun to(driver: WebDriver): CreateMessagePage {
driver.get("http://localhost:9990/mail/messages/form")
return PageFactory.initElements(driver, CreateMessagePage::class.java)
}
}
}
关键特性说明
- AbstractPage: 包含所有页面的通用功能(如导航栏、全局错误消息等)
- 自动元素解析: PageFactory 根据字段名自动查找对应的 HTML 元素
- @FindBy 注解: 提供灵活的元素查找策略,支持 CSS 选择器、XPath 等
结果验证
使用 AssertJ 进行断言验证:
kotlin
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")
领域对象集成
ViewMessagePage
可以直接返回领域模型对象:
kotlin
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())
这样我们就可以在断言中使用丰富的领域对象,而不是简单的字符串比较。
资源清理
测试完成后,记得关闭 WebDriver 实例:
kotlin
@AfterEach
fun destroy() {
if (::driver.isInitialized) {
driver.close()
}
}
高级配置:MockMvcHtmlUnitDriverBuilder 详解
基础配置回顾
kotlin
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
高级配置选项
kotlin
lateinit var driver: WebDriver
@BeforeEach
fun setup() {
driver = MockMvcHtmlUnitDriverBuilder
// 应用 MockMvcConfigurer(如 Spring Security)
.webAppContextSetup(context, springSecurity())
// 设置上下文路径(默认为 "")
.contextPath("")
// 默认只对 localhost 使用 MockMvc
// 以下配置将对 example.com 和 example.org 也使用 MockMvc
.useMockMvcForHosts("example.com", "example.org")
.build()
}
独立 MockMvc 配置
我们也可以单独配置 MockMvc 实例,然后提供给 Builder:
kotlin
val mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build()
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
.contextPath("")
.useMockMvcForHosts("example.com", "example.org")
.build()
TIP
虽然这种方式更加冗长,但通过构建独立的 MockMvc 实例,我们可以获得 MockMvc 的全部功能。
完整测试示例
让我们通过一个完整的测试示例来展示整个流程:
完整的消息创建测试示例
kotlin
@SpringBootTest
@AutoConfigureTestDatabase
class MessageControllerWebDriverTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var driver: WebDriver
@BeforeEach
fun setup() {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
@AfterEach
fun cleanup() {
if (::driver.isInitialized) {
driver.close()
}
}
@Test
fun `should create message successfully`() {
// Given
val expectedSummary = "Spring Boot Testing"
val expectedText = "Learning MockMvc with WebDriver"
// When - 导航到创建消息页面
val createPage = CreateMessagePage.to(driver)
// When - 填写并提交表单
val viewPage = createPage.createMessage(
ViewMessagePage::class.java,
expectedSummary,
expectedText
)
// Then - 验证结果
val actualMessage = viewPage.getMessage()
assertThat(actualMessage.summary).isEqualTo(expectedSummary)
assertThat(actualMessage.text).isEqualTo(expectedText)
assertThat(viewPage.success).isEqualTo("Successfully created a new message")
}
@Test
fun `should show validation error when summary is empty`() {
// Given
val createPage = CreateMessagePage.to(driver)
// When - 提交空的摘要
val resultPage = createPage.createMessage(
CreateMessagePage::class.java,
"", // 空摘要
"Some text"
)
// Then - 应该返回错误页面
assertThat(resultPage.hasErrors()).isTrue()
assertThat(resultPage.getErrorMessage()).contains("Summary is required")
}
}
技术对比:HtmlUnit vs WebDriver
最佳实践与建议
✅ 推荐做法
代码组织建议
- 使用 Page Object 模式:将页面逻辑封装到独立的类中
- 合理使用 @FindBy:对于复杂的元素选择器,使用注解而不是默认的字段名匹配
- 领域对象集成:让页面对象返回业务领域对象,而不是原始的 HTML 元素
- 资源管理:始终在测试结束后关闭 WebDriver 实例
⚠️ 注意事项
常见陷阱
- 元素查找失败:确保页面元素的 id 或 name 与字段名匹配
- 异步操作:WebDriver 可能需要等待页面加载完成
- 测试隔离:每个测试都应该有独立的 WebDriver 实例
总结
MockMvc 与 WebDriver 的集成为我们提供了一个强大而优雅的测试解决方案:
- 🎯 简化代码:Page Object 模式 + PageFactory 大大减少了样板代码
- 🔧 易于维护:元素定位逻辑集中管理,UI 变更时只需修改一处
- 🚀 提升效率:无需启动真实服务器,测试运行速度快
- 💪 功能强大:结合了 MockMvc 的强大功能和 WebDriver 的优雅 API
通过这种方式,我们不仅能够编写出更加清晰和可维护的测试代码,还能享受到快速的测试执行速度。这就是现代 Spring Boot 应用测试的最佳实践! 🎉