Appearance
MockMvc:Spring MVC 测试的利器 🧪
什么是 MockMvc?为什么需要它?
想象一下,你正在开发一个在线购物网站的后端服务。每次修改代码后,你都需要:
- 启动整个应用服务器
- 打开浏览器或 Postman
- 手动发送 HTTP 请求
- 检查返回结果
这个过程不仅耗时,而且容易出错。更糟糕的是,如果你的应用依赖数据库、消息队列等外部服务,测试环境的搭建会变得异常复杂。
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 让这一切变得简单而优雅。