Skip to content

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) 
        }
    }
}

关键特性说明

  1. AbstractPage: 包含所有页面的通用功能(如导航栏、全局错误消息等)
  2. 自动元素解析: PageFactory 根据字段名自动查找对应的 HTML 元素
  3. @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

最佳实践与建议

✅ 推荐做法

代码组织建议

  1. 使用 Page Object 模式:将页面逻辑封装到独立的类中
  2. 合理使用 @FindBy:对于复杂的元素选择器,使用注解而不是默认的字段名匹配
  3. 领域对象集成:让页面对象返回业务领域对象,而不是原始的 HTML 元素
  4. 资源管理:始终在测试结束后关闭 WebDriver 实例

⚠️ 注意事项

常见陷阱

  1. 元素查找失败:确保页面元素的 id 或 name 与字段名匹配
  2. 异步操作:WebDriver 可能需要等待页面加载完成
  3. 测试隔离:每个测试都应该有独立的 WebDriver 实例

总结

MockMvc 与 WebDriver 的集成为我们提供了一个强大而优雅的测试解决方案:

  • 🎯 简化代码:Page Object 模式 + PageFactory 大大减少了样板代码
  • 🔧 易于维护:元素定位逻辑集中管理,UI 变更时只需修改一处
  • 🚀 提升效率:无需启动真实服务器,测试运行速度快
  • 💪 功能强大:结合了 MockMvc 的强大功能和 WebDriver 的优雅 API

通过这种方式,我们不仅能够编写出更加清晰和可维护的测试代码,还能享受到快速的测试执行速度。这就是现代 Spring Boot 应用测试的最佳实践! 🎉