Appearance
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()) }
}
}
两种方式的对比分析
特性对比表
特性 | WebApplicationContext | Standalone |
---|---|---|
配置复杂度 | 低(自动配置) | 中(手动配置) |
测试范围 | 集成测试(包含 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
记住,测试不是非黑即白的选择。最佳实践是根据具体需求选择合适的方式,甚至在同一个项目中混合使用两种方式,构建一个健壮、高效的测试套件。
选择哪种方式,取决于你的测试目标:是要验证组件间的集成,还是要快速验证单个组件的行为。理解了这个核心区别,你就能在合适的场景下做出正确的选择! 🎉