Skip to content

MockMvc 测试配置详解:两种设置方式的深度解析 🚀

概述

在 Spring Boot 应用的测试中,MockMvc 是一个强大的工具,它允许我们在不启动完整 Web 服务器的情况下测试 Web 层。但是,你知道吗?MockMvc 提供了两种不同的设置方式,每种方式都有其独特的优势和适用场景。

IMPORTANT

MockMvc 的两种设置方式代表了测试策略中的经典选择:集成测试 vs 单元测试。理解它们的区别将帮助你选择最适合的测试策略。

MockMvc 的两种设置方式

1. WebApplicationContext 方式(集成测试风格)

这种方式会加载完整的 Spring MVC 配置,创建一个更接近真实环境的测试。

kotlin
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserControllerIntegrationTest {

    @Autowired
    private lateinit var webApplicationContext: WebApplicationContext
    
    private lateinit var mockMvc: MockMvc
    
    @MockBean
    private lateinit var userService: UserService // 模拟服务层依赖
    
    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders
            .webAppContextSetup(webApplicationContext) 
            .build()
    }
    
    @Test
    fun `should create user successfully`() {
        // 准备测试数据
        val userRequest = CreateUserRequest("张三", "[email protected]")
        val expectedUser = User(1L, "张三", "[email protected]")
        
        // 模拟服务层行为
        `when`(userService.createUser(any())).thenReturn(expectedUser)
        
        // 执行测试
        mockMvc.perform(
            post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(ObjectMapper().writeValueAsString(userRequest))
        )
        .andExpect(status().isCreated)
        .andExpect(jsonPath("$.name").value("张三"))
        .andExpect(jsonPath("$.email").value("[email protected]"))
    }
}

2. Standalone 方式(单元测试风格)

这种方式直接针对特定的控制器进行测试,手动配置 Spring MVC 基础设施。

kotlin
class UserControllerUnitTest {

    private val userService: UserService = mockk() 
    private val userController = UserController(userService) 
    private lateinit var mockMvc: MockMvc
    
    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders
            .standaloneSetup(userController) 
            .setControllerAdvice(GlobalExceptionHandler()) // 手动添加异常处理器
            .build()
    }
    
    @Test
    fun `should handle user creation with validation error`() {
        // 准备无效的请求数据
        val invalidRequest = CreateUserRequest("", "") // 空的姓名和邮箱
        
        // 执行测试(不需要模拟 service,因为验证在控制器层就会失败)
        mockMvc.perform(
            post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(ObjectMapper().writeValueAsString(invalidRequest))
        )
        .andExpect(status().isBadRequest)
        .andExpect(jsonPath("$.message").exists())
        
        // 验证 service 没有被调用
        verify(exactly = 0) { userService.createUser(any()) } 
    }
}

两种方式的对比分析

特性对比表

特性WebApplicationContextStandalone
配置复杂度低(自动配置)中(手动配置)
测试范围集成测试(包含 Spring 配置)单元测试(仅控制器)
启动速度慢(需加载 Spring 上下文)快(无需 Spring 上下文)
配置缓存✅ 支持(TestContext 框架)❌ 不支持
真实性高(接近生产环境)低(隔离环境)
调试便利性

实际业务场景示例

让我们通过一个电商订单管理的例子来看看两种方式的实际应用:

kotlin
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderControllerIntegrationTest {

    @Autowired
    private lateinit var webApplicationContext: WebApplicationContext
    
    @MockBean
    private lateinit var orderService: OrderService
    
    @MockBean
    private lateinit var paymentService: PaymentService
    
    private lateinit var mockMvc: MockMvc
    
    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders
            .webAppContextSetup(webApplicationContext)
            .build()
    }
    
    @Test
    fun `should process order with complete workflow`() {
        // 模拟完整的订单处理流程
        val orderRequest = CreateOrderRequest(
            userId = 1L,
            items = listOf(OrderItem("商品A", 2, 100.0))
        )
        
        val createdOrder = Order(
            id = 1L,
            userId = 1L,
            status = OrderStatus.PENDING,
            totalAmount = 200.0
        )
        
        // 模拟服务层交互
        `when`(orderService.createOrder(any())).thenReturn(createdOrder)
        `when`(paymentService.processPayment(any())).thenReturn(
            PaymentResult(success = true, transactionId = "TXN123")
        )
        
        // 测试完整的 HTTP 请求处理
        mockMvc.perform(
            post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(ObjectMapper().writeValueAsString(orderRequest))
                .header("Authorization", "Bearer valid-token") 
        )
        .andExpect(status().isCreated)
        .andExpect(jsonPath("$.id").value(1))
        .andExpect(jsonPath("$.status").value("PENDING"))
        .andExpect(jsonPath("$.totalAmount").value(200.0))
        
        // 验证服务层调用
        verify(orderService).createOrder(any())
    }
}
kotlin
class OrderControllerUnitTest {

    private val orderService: OrderService = mockk()
    private val paymentService: PaymentService = mockk()
    private val orderController = OrderController(orderService, paymentService)
    private lateinit var mockMvc: MockMvc
    
    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders
            .standaloneSetup(orderController)
            .setControllerAdvice(GlobalExceptionHandler())
            .build()
    }
    
    @Test
    fun `should validate order request parameters`() {
        // 专注于测试参数验证逻辑
        val invalidRequest = CreateOrderRequest(
            userId = null, 
            items = emptyList() 
        )
        
        mockMvc.perform(
            post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(ObjectMapper().writeValueAsString(invalidRequest))
        )
        .andExpect(status().isBadRequest)
        .andExpect(jsonPath("$.errors").isArray)
        .andExpect(jsonPath("$.errors[*].field").value(hasItems("userId", "items")))
        
        // 确保在验证失败时不会调用服务层
        verify(exactly = 0) { orderService.createOrder(any()) } 
        verify(exactly = 0) { paymentService.processPayment(any()) } 
    }
    
    @Test
    fun `should handle service layer exceptions properly`() {
        // 专注于测试异常处理逻辑
        val validRequest = CreateOrderRequest(
            userId = 1L,
            items = listOf(OrderItem("商品A", 1, 50.0))
        )
        
        // 模拟服务层抛出业务异常
        every { orderService.createOrder(any()) } throws 
            InsufficientStockException("商品A库存不足") 
        
        mockMvc.perform(
            post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(ObjectMapper().writeValueAsString(validRequest))
        )
        .andExpect(status().isConflict)
        .andExpect(jsonPath("$.message").value("商品A库存不足"))
        
        verify(exactly = 1) { orderService.createOrder(any()) }
    }
}

如何选择合适的测试方式?

使用 WebApplicationContext 的场景 ✅

TIP

当你需要验证完整的请求处理流程时,选择 WebApplicationContext 方式。

  • 集成测试:验证控制器、过滤器、拦截器等组件的协同工作
  • 配置验证:确保 Spring MVC 配置(如消息转换器、异常处理器)正常工作
  • 安全测试:测试 Spring Security 配置是否正确
  • 完整流程测试:验证从 HTTP 请求到响应的完整处理链
kotlin
@Test
fun `should apply security configuration correctly`() {
    // 测试未认证用户访问受保护资源
    mockMvc.perform(get("/api/admin/users"))
        .andExpect(status().isUnauthorized) 
    
    // 测试已认证用户访问
    mockMvc.perform(
        get("/api/admin/users")
            .header("Authorization", "Bearer valid-admin-token") 
    )
    .andExpect(status().isOk)
}

使用 Standalone 的场景 ✅

TIP

当你需要快速、专注地测试特定控制器逻辑时,选择 Standalone 方式。

  • 单元测试:专注于单个控制器的业务逻辑
  • 快速反馈:需要快速运行的测试(如 TDD 开发)
  • 边界条件测试:测试各种异常情况和边界条件
  • 调试特定问题:快速验证某个特定行为
kotlin
@Test
fun `should handle concurrent requests gracefully`() {
    // 模拟并发场景下的控制器行为
    every { orderService.createOrder(any()) } returns Order(1L, 1L, OrderStatus.PENDING, 100.0)
    
    // 快速测试控制器的并发处理能力
    val requests = (1..10).map {
        async {
            mockMvc.perform(
                post("/api/orders")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("""{"userId":$it,"items":[]}""")
            ).andReturn()
        }
    }
    
    // 验证所有请求都能正常处理
    requests.forEach { 
        assertEquals(201, it.await().response.status) 
    }
}

最佳实践建议

1. 混合使用策略 🎯

IMPORTANT

在实际项目中,建议同时使用两种方式,形成完整的测试金字塔。

kotlin
// 用 Standalone 进行快速单元测试
class UserControllerUnitTest { /* 快速验证业务逻辑 */ }

// 用 WebApplicationContext 进行关键路径集成测试
@SpringBootTest
class UserControllerIntegrationTest { /* 验证完整流程 */ }

2. 测试分层策略

测试金字塔

  • 底层(70%):Standalone 单元测试 - 快速验证各种边界条件
  • 中层(20%):WebApplicationContext 集成测试 - 验证关键业务流程
  • 顶层(10%):端到端测试 - 验证用户完整体验

3. 性能优化技巧

kotlin
// 为 WebApplicationContext 测试创建共享配置
@TestConfiguration
class TestConfig {
    @Bean
    @Primary
    fun mockUserService(): UserService = mockk()
    
    @Bean
    @Primary  
    fun mockOrderService(): OrderService = mockk()
}

// 使用相同配置的测试类会共享 Spring 上下文
@SpringBootTest(classes = [TestConfig::class])
class SharedContextTest1 { /* ... */ }

@SpringBootTest(classes = [TestConfig::class]) 
class SharedContextTest2 { /* ... */ } 

总结

MockMvc 的两种设置方式各有优势:

  • WebApplicationContext 提供了更真实的集成测试环境,适合验证完整的请求处理流程
  • Standalone 提供了更快速、专注的单元测试环境,适合验证特定的控制器逻辑

NOTE

记住,测试不是非黑即白的选择。最佳实践是根据具体需求选择合适的方式,甚至在同一个项目中混合使用两种方式,构建一个健壮、高效的测试套件。

选择哪种方式,取决于你的测试目标:是要验证组件间的集成,还是要快速验证单个组件的行为。理解了这个核心区别,你就能在合适的场景下做出正确的选择! 🎉