Appearance
MockMvc 配置详解:Spring Boot 测试的利器 🚀
什么是 MockMvc?为什么需要它?
在 Spring Boot 开发中,我们经常需要测试 Web 层的控制器(Controller)。传统的测试方式需要启动整个 Web 服务器,这样做有几个问题:
- 启动慢 ⏰:每次测试都要启动完整的 Web 容器
- 资源消耗大 💾:占用大量内存和 CPU
- 依赖复杂 🔗:需要配置数据库、缓存等外部依赖
NOTE
MockMvc 是 Spring Framework 提供的一个测试工具,它可以在不启动完整 Web 服务器的情况下,模拟 HTTP 请求和响应,让我们能够快速、轻量级地测试 Web 层逻辑。
MockMvc 的核心价值 💡
MockMvc 解决了以下核心痛点:
- 快速测试:无需启动 Web 服务器,测试执行速度快
- 精确控制:可以精确控制请求参数、请求头等
- 完整验证:可以验证响应状态码、响应体、响应头等
- 集成友好:与 Spring 测试框架完美集成
MockMvc 的两种配置方式
Spring 提供了两种配置 MockMvc 的方式,每种都有其适用场景:
方式一:独立模式(Standalone Setup)
这种方式直接指定要测试的控制器,并手动配置 Spring MVC 基础设施。
kotlin
class MyWebTests {
lateinit var mockMvc: MockMvc
@BeforeEach
fun setup() {
// 直接指定要测试的控制器
mockMvc = MockMvcBuilders
.standaloneSetup(AccountController())
.build()
}
@Test
fun testGetAccount() {
mockMvc.perform(get("/account/123"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.id").value(123))
}
}
kotlin
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
fun getAccount(@PathVariable id: Long): Account {
return Account(id = id, name = "张三", balance = 1000.0)
}
@PostMapping
fun createAccount(@RequestBody account: Account): Account {
// 模拟保存逻辑
return account.copy(id = 1L)
}
}
data class Account(
val id: Long? = null,
val name: String,
val balance: Double
)
TIP
独立模式适合:
- 单元测试单个控制器
- 不需要完整 Spring 上下文的场景
- 快速验证控制器逻辑
方式二:Web 应用上下文模式(WebApplicationContext Setup)
这种方式通过 Spring 配置来设置 MockMvc,包含完整的 Spring MVC 和控制器基础设施。
kotlin
@SpringJUnitWebConfig(locations = ["my-servlet-context.xml"])
class MyWebTests {
lateinit var mockMvc: MockMvc
@BeforeEach
fun setup(wac: WebApplicationContext) {
// 使用完整的 Spring 上下文
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.build()
}
@Test
fun testAccountWithValidation() {
val accountJson = """
{
"name": "",
"balance": -100
}
""".trimIndent()
mockMvc.perform(
post("/account")
.contentType(MediaType.APPLICATION_JSON)
.content(accountJson)
)
.andExpect(status().isBadRequest) // 验证参数校验
}
}
kotlin
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.H2)
class AccountControllerIntegrationTest {
@Autowired
lateinit var mockMvc: MockMvc
@Autowired
lateinit var accountRepository: AccountRepository
@Test
fun testCreateAccountWithDatabase() {
val accountJson = """
{
"name": "李四",
"balance": 2000.0
}
""".trimIndent()
mockMvc.perform(
post("/account")
.contentType(MediaType.APPLICATION_JSON)
.content(accountJson)
)
.andExpect(status().isCreated)
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.name").value("李四"))
// 验证数据库中确实保存了数据
val savedAccounts = accountRepository.findAll()
assertThat(savedAccounts).hasSize(1)
assertThat(savedAccounts[0].name).isEqualTo("李四")
}
}
IMPORTANT
Web 应用上下文模式适合:
- 集成测试,需要完整的 Spring 功能
- 测试过滤器、拦截器等 Web 组件
- 验证完整的请求处理流程
两种模式的对比分析
特性 | 独立模式 | Web上下文模式 |
---|---|---|
启动速度 | ⚡ 很快 | 🐌 较慢 |
内存消耗 | 💚 很少 | 🟡 较多 |
测试范围 | 🎯 单个控制器 | 🌐 完整Web层 |
依赖注入 | ❌ 需手动mock | ✅ 自动注入 |
配置复杂度 | 🟢 简单 | 🟡 复杂 |
适用场景 | 单元测试 | 集成测试 |
实际业务场景示例
让我们看一个更贴近实际业务的例子:
完整的用户管理系统测试示例
kotlin
// 用户实体
@Entity
data class User(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false, unique = true)
val username: String,
@Column(nullable = false)
val email: String,
@Column(nullable = false)
val password: String,
@Enumerated(EnumType.STRING)
val role: UserRole = UserRole.USER
)
enum class UserRole {
USER, ADMIN
}
// 用户控制器
@RestController
@RequestMapping("/api/users")
@Validated
class UserController(
private val userService: UserService
) {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): ResponseEntity<User> {
val user = userService.findById(id)
return ResponseEntity.ok(user)
}
@PostMapping
fun createUser(@Valid @RequestBody createUserRequest: CreateUserRequest): ResponseEntity<User> {
val user = userService.createUser(createUserRequest)
return ResponseEntity.status(HttpStatus.CREATED).body(user)
}
@GetMapping
fun getUsers(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int
): ResponseEntity<Page<User>> {
val users = userService.findAll(PageRequest.of(page, size))
return ResponseEntity.ok(users)
}
}
// 创建用户请求DTO
data class CreateUserRequest(
@field:NotBlank(message = "用户名不能为空")
@field:Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
val username: String,
@field:NotBlank(message = "邮箱不能为空")
@field:Email(message = "邮箱格式不正确")
val email: String,
@field:NotBlank(message = "密码不能为空")
@field:Size(min = 6, message = "密码长度至少6位")
val password: String
)
// 独立模式测试
class UserControllerStandaloneTest {
lateinit var mockMvc: MockMvc
@Mock
lateinit var userService: UserService
@InjectMocks
lateinit var userController: UserController
@BeforeEach
fun setup() {
MockitoAnnotations.openMocks(this)
mockMvc = MockMvcBuilders
.standaloneSetup(userController)
.setControllerAdvice(GlobalExceptionHandler()) // 添加异常处理
.build()
}
@Test
fun `should return user when user exists`() {
// Given
val userId = 1L
val user = User(id = userId, username = "testuser", email = "[email protected]", password = "password")
`when`(userService.findById(userId)).thenReturn(user)
// When & Then
mockMvc.perform(get("/api/users/{id}", userId))
.andExpect(status().isOk)
.andExpected(jsonPath("$.id").value(userId))
.andExpected(jsonPath("$.username").value("testuser"))
.andExpected(jsonPath("$.email").value("[email protected]"))
}
@Test
fun `should return 400 when create user with invalid data`() {
val invalidUserJson = """
{
"username": "ab",
"email": "invalid-email",
"password": "123"
}
""".trimIndent()
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidUserJson)
)
.andExpect(status().isBadRequest)
}
}
// Web上下文模式集成测试
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.H2)
@Transactional
class UserControllerIntegrationTest {
@Autowired
lateinit var mockMvc: MockMvc
@Autowired
lateinit var userRepository: UserRepository
@Test
fun `should create user successfully with valid data`() {
val createUserRequest = """
{
"username": "newuser",
"email": "[email protected]",
"password": "password123"
}
""".trimIndent()
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(createUserRequest)
)
.andExpect(status().isCreated)
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.username").value("newuser"))
.andExpect(jsonPath("$.email").value("[email protected]"))
// 验证数据库中确实保存了用户
val savedUser = userRepository.findByUsername("newuser")
assertThat(savedUser).isNotNull
assertThat(savedUser!!.email).isEqualTo("[email protected]")
}
@Test
fun `should return paginated users`() {
// 准备测试数据
repeat(15) { index ->
userRepository.save(
User(
username = "user$index",
email = "user$index@example.com",
password = "password"
)
)
}
// 测试分页查询
mockMvc.perform(
get("/api/users")
.param("page", "0")
.param("size", "10")
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.content").isArray)
.andExpect(jsonPath("$.content.length()").value(10))
.andExpect(jsonPath("$.totalElements").value(15))
.andExpect(jsonPath("$.totalPages").value(2))
}
}
最佳实践建议 🎯
选择合适的模式
- 单元测试:使用独立模式,专注测试控制器逻辑
- 集成测试:使用 Web 上下文模式,验证完整流程
- 性能考虑:大部分测试使用独立模式,少量关键路径使用集成测试
常见陷阱
- 不要在独立模式中依赖 Spring 的自动配置
- Web 上下文模式要注意测试数据的清理
- 避免过度使用集成测试,会拖慢测试套件执行速度
总结
MockMvc 是 Spring Boot 测试体系中的重要工具,它提供了两种灵活的配置方式:
- 独立模式:轻量级、快速,适合单元测试
- Web 上下文模式:完整功能、真实环境,适合集成测试
选择合适的模式,结合具体的业务场景,可以构建出既高效又可靠的测试体系。记住,好的测试不仅能发现问题,更能提升代码质量和开发效率! ✅