Appearance
Spring MockMvc 深度解析:让 Web 层测试变得简单高效 🚀
引言:为什么需要 MockMvc?
在 Spring Boot 开发中,我们经常需要测试 Controller 层的逻辑。传统的单元测试虽然可以测试控制器的方法,但它们无法验证以下关键功能:
- 🔗 请求映射(Request Mapping)
- 📝 数据绑定(Data Binding)
- 🔄 消息转换(Message Conversion)
- ⚡ 类型转换(Type Conversion)
- ✅ 数据验证(Validation)
- 🛠️ 支持性注解(
@InitBinder
、@ModelAttribute
、@ExceptionHandler
)
IMPORTANT
MockMvc 的核心价值在于:它能够在不启动完整服务器的情况下,提供接近真实环境的 Spring MVC 测试体验。
MockMvc 的设计哲学与工作原理
核心设计思想
MockMvc 的设计哲学可以概括为:轻量级但功能完整的服务端测试框架。它通过以下方式实现这一目标:
解决的核心痛点
kotlin
@Test
fun `传统单元测试 - 功能有限`() {
// 只能测试业务逻辑,无法验证Web层功能
val controller = UserController(userService)
val user = User(name = "张三", age = 25)
val result = controller.createUser(user)
// ❌ 无法验证:请求映射、数据绑定、验证注解等
assertEquals("success", result)
}
kotlin
@Test
fun `MockMvc测试 - 功能完整`() {
// ✅ 可以验证完整的Web层功能
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"name":"张三","age":25}""")
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(header().exists("Location"))
}
MockMvc 的三种使用方式
MockMvc 提供了三种不同的 API 风格,满足不同的测试需求:
1. 原生 MockMvc API
kotlin
@WebMvcTest(UserController::class)
class UserControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var userService: UserService
@Test
fun `测试创建用户`() {
// 准备测试数据
val user = User(name = "李四", email = "[email protected]")
`when`(userService.createUser(any())).thenReturn(user.copy(id = 1L))
// 执行请求并验证
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user))
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("李四"))
.andDo(print()) // 打印请求和响应详情
}
}
2. MockMvcTester API(AssertJ 风格)
kotlin
@WebMvcTest(UserController::class)
class UserControllerAssertJTest {
@Autowired
private lateinit var mockMvcTester: MockMvcTester
@Test
fun `使用AssertJ风格测试`() {
val user = User(name = "王五", email = "[email protected]")
mockMvcTester
.post("/api/users") {
contentType(MediaType.APPLICATION_JSON)
content(objectMapper.writeValueAsString(user))
}
.assertThat()
.hasStatus(HttpStatus.CREATED)
.bodyJson()
.extractingPath("$.name").isEqualTo("王五")
}
}
3. WebTestClient API(响应式风格)
kotlin
@WebMvcTest(UserController::class)
class UserControllerWebTestClientTest {
@Autowired
private lateinit var webTestClient: WebTestClient
@Test
fun `使用WebTestClient测试`() {
val user = User(name = "赵六", email = "[email protected]")
webTestClient
.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(user)
.exchange()
.expectStatus().isCreated
.expectBody()
.jsonPath("$.name").isEqualTo("赵六")
}
}
实战案例:完整的用户管理 API 测试
让我们通过一个完整的用户管理系统来展示 MockMvc 的强大功能:
用户实体类和控制器代码
kotlin
// 用户实体
data class User(
val id: Long? = null,
@field:NotBlank(message = "用户名不能为空")
val name: String,
@field:Email(message = "邮箱格式不正确")
val email: String,
@field:Min(value = 18, message = "年龄不能小于18岁")
val age: Int
)
// 用户控制器
@RestController
@RequestMapping("/api/users")
@Validated
class UserController(private val userService: UserService) {
@PostMapping
fun createUser(@Valid @RequestBody user: User): ResponseEntity<User> {
val savedUser = userService.createUser(user)
return ResponseEntity
.status(HttpStatus.CREATED)
.header("Location", "/api/users/${savedUser.id}")
.body(savedUser)
}
@GetMapping("/{id}")
fun getUserById(@PathVariable id: Long): ResponseEntity<User> {
return userService.findById(id)
?.let { ResponseEntity.ok(it) }
?: ResponseEntity.notFound().build()
}
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<Map<String, String>> {
val errors = ex.bindingResult.fieldErrors.associate {
it.field to (it.defaultMessage ?: "验证失败")
}
return ResponseEntity.badRequest().body(errors)
}
}
综合测试套件
kotlin
@WebMvcTest(UserController::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserControllerIntegrationTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var objectMapper: ObjectMapper
@MockBean
private lateinit var userService: UserService
@Test
fun `成功创建用户`() {
// Given: 准备测试数据
val inputUser = User(
name = "张三",
email = "[email protected]",
age = 25
)
val savedUser = inputUser.copy(id = 1L)
`when`(userService.createUser(any())).thenReturn(savedUser)
// When & Then: 执行请求并验证结果
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(inputUser))
)
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/users/1"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andDo(print())
}
@Test
fun `数据验证失败测试`() {
// Given: 准备无效数据
val invalidUser = User(
name = "",
email = "invalid-email",
age = 16
)
// When & Then: 验证验证注解是否生效
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidUser))
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.name").value("用户名不能为空"))
.andExpect(jsonPath("$.email").value("邮箱格式不正确"))
.andExpect(jsonPath("$.age").value("年龄不能小于18岁"))
}
@Test
fun `查询不存在的用户`() {
// Given: 模拟服务返回空值
`when`(userService.findById(999L)).thenReturn(null)
// When & Then: 验证404响应
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
}
@Test
fun `查询存在的用户`() {
// Given: 准备用户数据
val existingUser = User(
id = 1L,
name = "李四",
email = "[email protected]",
age = 30
)
`when`(userService.findById(1L)).thenReturn(existingUser)
// When & Then: 验证成功响应
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("李四"))
}
}
MockMvc 的高级特性
1. 自定义请求构建器
kotlin
@Test
fun `测试文件上传`() {
val file = MockMultipartFile(
"avatar",
"avatar.jpg",
"image/jpeg",
"fake image content".toByteArray()
)
mockMvc.perform(
multipart("/api/users/1/avatar")
.file(file)
.param("description", "用户头像")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("头像上传成功"))
}
2. 会话和安全测试
kotlin
@Test
@WithMockUser(username = "admin", roles = ["ADMIN"])
fun `测试需要管理员权限的接口`() {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent())
verify(userService).deleteUser(1L)
}
@Test
fun `测试未授权访问`() {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isUnauthorized())
}
3. 响应内容验证
kotlin
@Test
fun `详细的响应验证`() {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].name").value("张三"))
.andExpect(jsonPath("$[1].name").value("李四"))
}
最佳实践与注意事项
MockMvc 使用技巧
- 使用 @WebMvcTest:只加载 Web 层相关的 Bean,测试启动更快
- 合理使用 @MockBean:模拟依赖服务,专注测试 Controller 逻辑
- 充分利用 JsonPath:验证 JSON 响应的结构和内容
- 使用 andDo(print()):在调试时打印请求和响应详情
常见陷阱
- MockMvc 测试的是 Spring MVC 层,不包括真实的 HTTP 传输
- 需要正确配置 ObjectMapper 以确保 JSON 序列化/反序列化正常工作
- 注意区分 @WebMvcTest 和 @SpringBootTest 的使用场景
性能考虑
MockMvc 测试比完整的集成测试快得多,但比纯单元测试慢。在测试金字塔中,它位于中间层,应该适度使用。
总结
MockMvc 是 Spring 生态系统中一个设计精良的测试工具,它完美地平衡了测试完整性和执行效率。通过 MockMvc,我们可以:
✅ 验证完整的 Web 层功能:请求映射、数据绑定、验证、异常处理等
✅ 无需启动服务器:测试执行速度快,资源消耗少
✅ 提供多种 API 风格:满足不同团队的编码偏好
✅ 集成 Spring 生态:与 Spring Security、Spring Data 等完美配合
掌握 MockMvc,让你的 Spring Boot Web 应用测试变得更加专业和高效! 🎯