Skip to content

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

精准测试,快速反馈,稳定可靠

核心价值总结

  1. 🚀 性能优势:快速启动,轻量级测试
  2. 🎯 精准测试:专注Web层逻辑,避免外部干扰
  3. 🛡️ 稳定可靠:隔离外部依赖,测试结果一致
  4. 📈 易于维护:清晰的测试结构,便于重构

最佳实践清单 ✅

  • ✅ 使用 @WebMvcTest 进行Controller单元测试
  • ✅ 合理使用 @MockBean Mock业务依赖
  • ✅ 编写完整的异常场景测试
  • ✅ 使用有意义的测试方法名和显示名
  • ✅ 验证HTTP状态码、响应头、响应体
  • ✅ 测试安全配置和权限控制
  • ❌ 避免过度Mock导致测试失真
  • ❌ 不要忽略边界条件和异常情况

通过掌握 MockMvc,你将能够构建高质量、高效率的Web层测试,为你的Spring Boot应用提供坚实的质量保障! 🎉