Skip to content

MockMvc:Spring MVC 测试的利器 🧪

什么是 MockMvc?为什么需要它?

想象一下,你正在开发一个在线购物网站的后端服务。每次修改代码后,你都需要:

  1. 启动整个应用服务器
  2. 打开浏览器或 Postman
  3. 手动发送 HTTP 请求
  4. 检查返回结果

这个过程不仅耗时,而且容易出错。更糟糕的是,如果你的应用依赖数据库、消息队列等外部服务,测试环境的搭建会变得异常复杂。

NOTE

MockMvc 就是为了解决这个痛点而生的!它让你能够在不启动真实服务器的情况下,完整地测试 Spring MVC 应用的请求处理流程。

MockMvc 的核心价值

MockMvc 提供了一种轻量级的测试方式,它:

  • 🚀 快速:无需启动完整的 Web 服务器
  • 🎯 精确:可以精确控制测试环境和数据
  • 🔧 灵活:支持多种断言方式(Hamcrest、AssertJ)
  • 🧩 集成:与 Spring 测试框架完美集成

MockMvc 的工作原理

让我们通过一个时序图来理解 MockMvc 的工作流程:

TIP

MockMvc 模拟了完整的 Spring MVC 请求处理流程,但使用的是模拟的请求和响应对象,而不是真实的 HTTP 通信。

快速上手:第一个 MockMvc 测试

让我们从一个简单的商品管理 API 开始:

kotlin
@RestController
@RequestMapping("/api/products")
class ProductController(
    private val productService: ProductService
) {
    
    @GetMapping("/{id}")
    fun getProduct(@PathVariable id: Long): ResponseEntity<Product> {
        val product = productService.findById(id)
        return ResponseEntity.ok(product)
    }
    
    @PostMapping
    fun createProduct(@RequestBody @Valid product: Product): ResponseEntity<Product> {
        val savedProduct = productService.save(product)
        return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct)
    }
}
kotlin
data class Product(
    val id: Long? = null,
    @field:NotBlank(message = "商品名称不能为空")
    val name: String,
    @field:DecimalMin(value = "0.0", message = "价格不能为负数")
    val price: BigDecimal,
    val description: String? = null
)

现在让我们编写 MockMvc 测试:

kotlin
@WebMvcTest(ProductController::class)
class ProductControllerTest {
    
    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @MockBean
    private lateinit var productService: ProductService
    
    @Test
    fun `should return product when valid id provided`() {
        // 准备测试数据
        val productId = 1L
        val expectedProduct = Product(
            id = productId,
            name = "iPhone 15",
            price = BigDecimal("999.99"),
            description = "最新款 iPhone"
        )
        
        // 模拟 Service 行为
        given(productService.findById(productId)).willReturn(expectedProduct)
        
        // 执行请求并验证结果
        mockMvc.perform(
            get("/api/products/{id}", productId) 
                .contentType(MediaType.APPLICATION_JSON)
        )
        .andExpect(status().isOk) 
        .andExpect(jsonPath("$.id").value(productId)) 
        .andExpect(jsonPath("$.name").value("iPhone 15")) 
        .andExpect(jsonPath("$.price").value(999.99)) 
    }
    
    @Test
    fun `should create product when valid data provided`() {
        val newProduct = Product(
            name = "MacBook Pro",
            price = BigDecimal("2499.99"),
            description = "专业级笔记本"
        )
        
        val savedProduct = newProduct.copy(id = 2L)
        given(productService.save(any())).willReturn(savedProduct)
        
        mockMvc.perform(
            post("/api/products") 
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {
                        "name": "MacBook Pro",
                        "price": 2499.99,
                        "description": "专业级笔记本"
                    }
                """.trimIndent())
        )
        .andExpect(status().isCreated) 
        .andExpect(jsonPath("$.id").value(2)) 
        .andExpect(jsonPath("$.name").value("MacBook Pro")) 
    }
}

IMPORTANT

注意 @WebMvcTest 注解的使用。它只会加载 Web 层的组件,不会启动完整的 Spring 上下文,这使得测试更快、更专注。

MockMvc 的三种使用方式

1. 原生 MockMvc + Hamcrest

这是最传统的方式,使用 Hamcrest 匹配器进行断言:

kotlin
@Test
fun `test with hamcrest matchers`() {
    mockMvc.perform(get("/api/products/1"))
        .andExpect(status().isOk)
        .andExpect(jsonPath("$.name", `is`("iPhone 15"))) 
        .andExpect(jsonPath("$.price", greaterThan(0.0))) 
}

2. MockMvcTester + AssertJ

Spring 6.1+ 引入的新方式,提供更流畅的 API:

kotlin
@Autowired
private lateinit var mockMvcTester: MockMvcTester

@Test
fun `test with assertj style`() {
    mockMvcTester.get("/api/products/1")
        .exchange() 
        .expectStatus().isOk 
        .expectBody().jsonPath("$.name").isEqualTo("iPhone 15") 
}

3. WebTestClient + MockMvc

结合 WebTestClient 的高级功能:

kotlin
@Test
fun `test with web test client`() {
    val client = WebTestClient.bindToController(ProductController(productService))
        .build()
    
    client.get().uri("/api/products/1")
        .exchange() 
        .expectStatus().isOk 
        .expectBody<Product>() 
        .consumeWith { result ->
            assertThat(result.responseBody?.name).isEqualTo("iPhone 15")
        }
}

高级测试场景

测试表单验证

kotlin
@Test
fun `should return validation errors when invalid product data`() {
    mockMvc.perform(
        post("/api/products")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {
                    "name": "",
                    "price": -100.0
                }
            """.trimIndent())
    )
    .andExpect(status().isBadRequest) 
    .andExpect(jsonPath("$.errors").isArray) 
    .andExpect(jsonPath("$.errors[*].field").value(hasItems("name", "price")))
}

测试文件上传

kotlin
@Test
fun `should upload product image successfully`() {
    val file = MockMultipartFile(
        "image",
        "product.jpg",
        MediaType.IMAGE_JPEG_VALUE,
        "fake image content".toByteArray()
    )
    
    mockMvc.perform(
        multipart("/api/products/1/image") 
            .file(file)
    )
    .andExpect(status().isOk)
    .andExpect(jsonPath("$.message").value("图片上传成功"))
}

测试安全认证

kotlin
@Test
@WithMockUser(roles = ["ADMIN"]) 
fun `should allow admin to delete product`() {
    mockMvc.perform(delete("/api/products/1"))
        .andExpect(status().isNoContent)
}

@Test
fun `should reject unauthorized delete request`() {
    mockMvc.perform(delete("/api/products/1"))
        .andExpect(status().isUnauthorized) 
}

MockMvc vs 端到端测试

让我们比较一下不同测试方式的特点:

测试方式启动时间测试范围隔离性真实性适用场景
MockMvc⚡ 快Web 层🔒 高📊 中等单元测试、集成测试
TestRestTemplate🐌 慢全栈📂 中等🎯 高集成测试
端到端测试🐌 很慢全系统📂 低🎯 最高系统测试、验收测试

TIP

推荐使用测试金字塔策略:

  • 70% 单元测试(包括 MockMvc)
  • 20% 集成测试
  • 10% 端到端测试

最佳实践与注意事项

✅ 推荐做法

测试数据管理

使用 @Sql 注解或测试数据构建器来管理测试数据,保持测试的可重复性。

kotlin
@Test
@Sql("/test-data/products.sql") 
fun `should return all products from database`() {
    mockMvc.perform(get("/api/products"))
        .andExpect(status().isOk)
        .andExpect(jsonPath("$.length()").value(3))
}

使用测试配置

创建专门的测试配置类,避免测试污染生产配置。

kotlin
@TestConfiguration
class TestConfig {
    
    @Bean
    @Primary
    fun testProductService(): ProductService {
        return mockk<ProductService>()
    }
}

⚠️ 常见陷阱

过度模拟

不要模拟所有依赖,适当保留一些真实组件可以提高测试的可信度。

测试脆弱性

避免测试过于依赖具体的 JSON 结构,使用 JsonPath 的灵活匹配。

kotlin
// ❌ 脆弱的测试
.andExpect(content().json("""{"id":1,"name":"iPhone"}"""))

// ✅ 更健壮的测试
.andExpect(jsonPath("$.id").exists()) 
.andExpect(jsonPath("$.name").isNotEmpty()) 

总结

MockMvc 是 Spring MVC 测试的核心工具,它让我们能够:

  • 🎯 专注测试:只测试 Web 层逻辑,隔离外部依赖
  • 快速反馈:无需启动完整服务器,测试执行速度快
  • 🔧 灵活断言:支持多种断言方式,满足不同测试需求
  • 🧩 完美集成:与 Spring 生态系统无缝集成

通过合理使用 MockMvc,你可以构建一套高效、可靠的测试体系,为代码质量保驾护航! 🚀

NOTE

记住,好的测试不仅要覆盖正常流程,更要覆盖异常情况。MockMvc 让这一切变得简单而优雅。