Skip to content

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 使用技巧

  1. 使用 @WebMvcTest:只加载 Web 层相关的 Bean,测试启动更快
  2. 合理使用 @MockBean:模拟依赖服务,专注测试 Controller 逻辑
  3. 充分利用 JsonPath:验证 JSON 响应的结构和内容
  4. 使用 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 应用测试变得更加专业和高效! 🎯