Skip to content

MockMvc 配置详解:Spring Boot 测试的利器 🚀

什么是 MockMvc?为什么需要它?

在 Spring Boot 开发中,我们经常需要测试 Web 层的控制器(Controller)。传统的测试方式需要启动整个 Web 服务器,这样做有几个问题:

  • 启动慢 ⏰:每次测试都要启动完整的 Web 容器
  • 资源消耗大 💾:占用大量内存和 CPU
  • 依赖复杂 🔗:需要配置数据库、缓存等外部依赖

NOTE

MockMvc 是 Spring Framework 提供的一个测试工具,它可以在不启动完整 Web 服务器的情况下,模拟 HTTP 请求和响应,让我们能够快速、轻量级地测试 Web 层逻辑。

MockMvc 的核心价值 💡

MockMvc 解决了以下核心痛点:

  1. 快速测试:无需启动 Web 服务器,测试执行速度快
  2. 精确控制:可以精确控制请求参数、请求头等
  3. 完整验证:可以验证响应状态码、响应体、响应头等
  4. 集成友好:与 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 测试体系中的重要工具,它提供了两种灵活的配置方式:

  1. 独立模式:轻量级、快速,适合单元测试
  2. Web 上下文模式:完整功能、真实环境,适合集成测试

选择合适的模式,结合具体的业务场景,可以构建出既高效又可靠的测试体系。记住,好的测试不仅能发现问题,更能提升代码质量和开发效率! ✅