Appearance
Spring MockMvc 深度实战指南 🚀
引言:为什么需要 MockMvc?
在 Spring Boot 应用开发中,我们经常面临一个棘手的问题:如何高效地测试 Web 层(Controller)的逻辑?
IMPORTANT
想象一下,如果没有 MockMvc,我们要测试一个 REST API,就必须启动整个应用服务器,发送真实的 HTTP 请求,这不仅耗时,还可能因为网络、数据库等外部依赖导致测试不稳定。
MockMvc 的出现,就是为了解决这个核心痛点:在不启动完整 Web 服务器的情况下,模拟 HTTP 请求和响应,专注测试 Controller 层的业务逻辑。
MockMvc 的核心价值与设计哲学 💡
设计哲学:轻量级 + 精准测试
MockMvc 遵循 "测试金字塔" 理念,它位于单元测试和集成测试之间,提供了一个完美的平衡点:
核心优势对比
kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@Autowired
private lateinit var restTemplate: TestRestTemplate
@Test
fun `创建用户 - 需要启动完整服务器`() {
// 问题1:启动时间长(5-10秒)
// 问题2:需要真实数据库连接
// 问题3:端口冲突风险
// 问题4:外部依赖影响测试稳定性
val response = restTemplate.postForEntity(
"/api/users",
CreateUserRequest("张三", "[email protected]"),
UserResponse::class.java
)
assertThat(response.statusCode).isEqualTo(HttpStatus.CREATED)
}
}
kotlin
@WebMvcTest(UserController::class)
class UserControllerMockMvcTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var userService: UserService
@Test
fun `创建用户 - 轻量级快速测试`() {
// 优势1:启动时间快(1-2秒)
// 优势2:只加载Web层,隔离外部依赖
// 优势3:精准测试Controller逻辑
// 优势4:测试结果稳定可靠
given(userService.createUser(any())).willReturn(
User(1L, "张三", "[email protected]")
)
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"name":"张三","email":"[email protected]"}""")
)
.andExpect(status().isCreated)
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(jsonPath("$.email").value("[email protected]"))
}
}
MockMvc 核心概念深度解析 🔍
1. MockMvc 的工作原理
2. 关键注解深度理解
TIP
MockMvc 测试中的注解选择直接影响测试的范围和性能,理解它们的区别至关重要。
注解 | 加载范围 | 适用场景 | 性能 |
---|---|---|---|
@WebMvcTest | 仅Web层 | Controller单元测试 | ⚡ 最快 |
@SpringBootTest | 完整应用上下文 | 集成测试 | 🐌 较慢 |
@AutoConfigureTestDatabase | 测试数据库配置 | 数据层测试 | 🔄 中等 |
实战场景:构建完整的用户管理API测试 🛠️
让我们通过一个完整的用户管理系统来展示 MockMvc 的强大功能:
场景设定:用户管理系统
kotlin
// 用户实体
data class User(
val id: Long? = null,
val name: String,
val email: String,
val status: UserStatus = UserStatus.ACTIVE
)
enum class UserStatus { ACTIVE, INACTIVE, DELETED }
// 请求/响应 DTO
data class CreateUserRequest(val name: String, val email: String)
data class UpdateUserRequest(val name: String?, val email: String?)
data class UserResponse(val id: Long, val name: String, val email: String, val status: String)
Controller 实现
点击查看完整的 UserController 实现
kotlin
@RestController
@RequestMapping("/api/users")
@Validated
class UserController(private val userService: UserService) {
@PostMapping
fun createUser(@Valid @RequestBody request: CreateUserRequest): ResponseEntity<UserResponse> {
val user = userService.createUser(request)
return ResponseEntity.status(HttpStatus.CREATED).body(user.toResponse())
}
@GetMapping("/{id}")
fun getUserById(@PathVariable id: Long): ResponseEntity<UserResponse> {
val user = userService.findById(id)
return ResponseEntity.ok(user.toResponse())
}
@PutMapping("/{id}")
fun updateUser(
@PathVariable id: Long,
@Valid @RequestBody request: UpdateUserRequest
): ResponseEntity<UserResponse> {
val user = userService.updateUser(id, request)
return ResponseEntity.ok(user.toResponse())
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable id: Long): ResponseEntity<Void> {
userService.deleteUser(id)
return ResponseEntity.noContent().build()
}
@GetMapping
fun getAllUsers(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int
): ResponseEntity<Page<UserResponse>> {
val users = userService.findAll(PageRequest.of(page, size))
return ResponseEntity.ok(users.map { it.toResponse() })
}
private fun User.toResponse() = UserResponse(id!!, name, email, status.name)
}
全面的 MockMvc 测试套件
kotlin
@WebMvcTest(UserController::class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var userService: UserService
@Autowired
private lateinit var objectMapper: ObjectMapper
companion object {
private const val BASE_URL = "/api/users"
}
@Nested
@DisplayName("用户创建测试")
inner class CreateUserTests {
@Test
fun `创建用户成功 - 返回201状态码和用户信息`() {
// Given: 准备测试数据
val request = CreateUserRequest("张三", "[email protected]")
val createdUser = User(1L, "张三", "[email protected]", UserStatus.ACTIVE)
given(userService.createUser(request)).willReturn(createdUser)
// When & Then: 执行请求并验证结果
mockMvc.perform(
post(BASE_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isCreated)
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.status").value("ACTIVE"))
// 验证Service方法被正确调用
verify(userService).createUser(request)
}
@Test
fun `创建用户失败 - 邮箱格式无效`() {
val invalidRequest = CreateUserRequest("张三", "invalid-email")
mockMvc.perform(
post(BASE_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidRequest))
)
.andExpect(status().isBadRequest)
.andExpect(jsonPath("$.message").exists())
// 验证Service方法未被调用
verifyNoInteractions(userService)
}
@Test
fun `创建用户失败 - 邮箱已存在`() {
val request = CreateUserRequest("张三", "[email protected]")
given(userService.createUser(request))
.willThrow(EmailAlreadyExistsException("邮箱已存在"))
mockMvc.perform(
post(BASE_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isConflict)
.andExpect(jsonPath("$.message").value("邮箱已存在"))
}
}
@Nested
@DisplayName("用户查询测试")
inner class GetUserTests {
@Test
fun `根据ID查询用户成功`() {
val userId = 1L
val user = User(userId, "张三", "[email protected]", UserStatus.ACTIVE)
given(userService.findById(userId)).willReturn(user)
mockMvc.perform(get("$BASE_URL/$userId"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.name").value("张三"))
}
@Test
fun `查询不存在的用户 - 返回404`() {
val userId = 999L
given(userService.findById(userId))
.willThrow(UserNotFoundException("用户不存在"))
mockMvc.perform(get("$BASE_URL/$userId"))
.andExpect(status().isNotFound)
.andExpect(jsonPath("$.message").value("用户不存在"))
}
}
@Nested
@DisplayName("用户更新测试")
inner class UpdateUserTests {
@Test
fun `更新用户信息成功`() {
val userId = 1L
val updateRequest = UpdateUserRequest("李四", "[email protected]")
val updatedUser = User(userId, "李四", "[email protected]", UserStatus.ACTIVE)
given(userService.updateUser(userId, updateRequest)).willReturn(updatedUser)
mockMvc.perform(
put("$BASE_URL/$userId")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.name").value("李四"))
.andExpect(jsonPath("$.email").value("[email protected]"))
}
}
@Nested
@DisplayName("用户删除测试")
inner class DeleteUserTests {
@Test
fun `删除用户成功 - 返回204状态码`() {
val userId = 1L
doNothing().`when`(userService).deleteUser(userId)
mockMvc.perform(delete("$BASE_URL/$userId"))
.andExpect(status().isNoContent)
.andExpect(content().string(""))
verify(userService).deleteUser(userId)
}
}
@Nested
@DisplayName("分页查询测试")
inner class PaginationTests {
@Test
fun `分页查询用户列表`() {
val users = listOf(
User(1L, "张三", "[email protected]"),
User(2L, "李四", "[email protected]")
)
val page = PageImpl(users, PageRequest.of(0, 10), 2)
given(userService.findAll(any<Pageable>())).willReturn(page)
mockMvc.perform(
get(BASE_URL)
.param("page", "0")
.param("size", "10")
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.content").isArray)
.andExpect(jsonPath("$.content.length()").value(2))
.andExpect(jsonPath("$.totalElements").value(2))
.andExpect(jsonPath("$.totalPages").value(1))
}
}
}
MockMvc 高级技巧与最佳实践 🎯
1. 自定义测试配置
kotlin
@TestConfiguration
class MockMvcTestConfig {
@Bean
@Primary
fun testObjectMapper(): ObjectMapper {
return ObjectMapper().apply {
registerModule(JavaTimeModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
}
}
}
2. 测试安全配置
kotlin
@WebMvcTest(UserController::class)
@Import(SecurityConfig::class)
class SecureUserControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
@WithMockUser(roles = ["ADMIN"])
fun `管理员可以删除用户`() {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent)
}
@Test
fun `未认证用户无法访问`() {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isUnauthorized)
}
}
3. 异常处理测试
kotlin
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException::class)
fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse("USER_NOT_FOUND", ex.message))
}
@ExceptionHandler(EmailAlreadyExistsException::class)
fun handleEmailExists(ex: EmailAlreadyExistsException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse("EMAIL_EXISTS", ex.message))
}
}
data class ErrorResponse(val code: String, val message: String?)
MockMvc vs 其他测试方式对比 ⚡
性能对比测试
kotlin
@WebMvcTest(UserController::class)
class UserControllerMockMvcTest {
// 启动时间:~1-2秒
// 内存占用:~50MB
// 测试隔离:✅ 优秀
// 外部依赖:✅ 完全隔离
}
kotlin
@SpringBootTest(webEnvironment = RANDOM_PORT)
class UserControllerIntegrationTest {
// 启动时间:~5-10秒
// 内存占用:~200MB
// 测试隔离:❌ 较差
// 外部依赖:❌ 需要真实环境
}
kotlin
@SpringBootTest(webEnvironment = RANDOM_PORT)
class UserControllerRestTemplateTest {
// 适用于:端到端测试
// 优势:真实HTTP调用
// 劣势:启动慢,依赖多
}
常见陷阱与解决方案 ⚠️
陷阱1:过度Mock导致测试失真
WARNING
不要Mock所有依赖,这会让测试失去意义
kotlin
@Test
fun `错误示例 - 过度Mock`() {
// 连HTTP状态码都Mock了,测试毫无意义
given(mockMvc.perform(any())).willReturn(
MockMvcResultMatchers.status().isOk
)
}
kotlin
@Test
fun `正确示例 - 合理Mock`() {
// 只Mock业务逻辑层,让Web层真实执行
given(userService.createUser(any())).willReturn(
User(1L, "张三", "[email protected]")
)
mockMvc.perform(post("/api/users")...)
.andExpect(status().isCreated) // 真实验证HTTP状态
}
陷阱2:忽略异常场景测试
CAUTION
异常场景往往是生产环境的主要问题源头,必须充分测试
kotlin
@Test
fun `测试各种异常场景`() {
// 网络异常
given(userService.createUser(any()))
.willThrow(ConnectException("数据库连接失败"))
// 业务异常
given(userService.createUser(any()))
.willThrow(BusinessException("业务规则违反"))
// 验证异常处理是否正确
mockMvc.perform(post("/api/users")...)
.andExpect(status().isInternalServerError)
.andExpect(jsonPath("$.message").exists())
}
总结:MockMvc 的价值与未来 🌟
MockMvc 不仅仅是一个测试工具,它代表了一种测试哲学:
NOTE
精准测试,快速反馈,稳定可靠
核心价值总结
- 🚀 性能优势:快速启动,轻量级测试
- 🎯 精准测试:专注Web层逻辑,避免外部干扰
- 🛡️ 稳定可靠:隔离外部依赖,测试结果一致
- 📈 易于维护:清晰的测试结构,便于重构
最佳实践清单 ✅
- ✅ 使用
@WebMvcTest
进行Controller单元测试 - ✅ 合理使用
@MockBean
Mock业务依赖 - ✅ 编写完整的异常场景测试
- ✅ 使用有意义的测试方法名和显示名
- ✅ 验证HTTP状态码、响应头、响应体
- ✅ 测试安全配置和权限控制
- ❌ 避免过度Mock导致测试失真
- ❌ 不要忽略边界条件和异常情况
通过掌握 MockMvc,你将能够构建高质量、高效率的Web层测试,为你的Spring Boot应用提供坚实的质量保障! 🎉