Skip to content

MockMvc vs 端到端测试:选择合适的测试策略 🎯

引言:为什么需要理解测试的边界?

在 Spring Boot 开发中,测试是保证代码质量的重要环节。但是,面对 MockMvc 和端到端测试,很多初学者会困惑:什么时候用 MockMvc?什么时候用端到端测试?它们各自解决什么问题?

IMPORTANT

理解不同测试策略的本质差异,能帮助我们在合适的场景选择合适的工具,既保证测试效果,又提高开发效率。

MockMvc 的本质:模拟中的真实

🔍 MockMvc 是什么?

MockMvc 是 Spring Test 模块提供的测试工具,它基于 Servlet API 的模拟实现,不依赖真实的容器运行

💡 MockMvc 的核心特点

kotlin
@WebMvcTest(UserController::class)
class UserControllerTest {
    
    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @MockBean
    private lateinit var userService: UserService
    
    @Test
    fun `should return user when valid id provided`() {
        // 准备测试数据
        val userId = 1L
        val mockUser = User(userId, "张三", "[email protected]")
        
        // 模拟服务层行为
        `when`(userService.findById(userId)).thenReturn(mockUser) 
        
        // 执行测试
        mockMvc.perform(get("/api/users/$userId"))
            .andExpect(status().isOk) 
            .andExpect(jsonPath("$.name").value("张三")) 
            .andExpect(jsonPath("$.email").value("[email protected]"))
    }
}
kotlin
@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): User {
        return userService.findById(id) 
    }
}

NOTE

注意上面的例子中,userService.findById() 被模拟了,不会真正访问数据库。这就是 MockMvc 的特点:专注测试 Web 层逻辑

MockMvc 的限制:理解边界很重要

🚫 MockMvc 不能做什么?

MockMvc 基于模拟的 MockHttpServletRequest,这意味着:

MockMvc 的限制

  • 没有默认的上下文路径
  • 没有 jsessionid cookie
  • 不支持转发、错误或异步分发
  • JSP 不会被真正渲染(只能验证转发的 JSP 页面路径)
kotlin
@Test
fun `MockMvc JSP 测试示例`() {
    mockMvc.perform(get("/users/list"))
        .andExpect(status().isOk)
        .andExpect(view().name("users/list")) 
        .andExpect(forwardedUrl("/WEB-INF/views/users/list.jsp")) 
    
    // 注意:这里只能验证转发的路径,JSP 不会真正渲染 HTML
}

✅ MockMvc 能做什么?

对于不依赖转发的渲染技术,MockMvc 工作得很好:

kotlin
@Test
fun `should render thymeleaf template correctly`() {
    mockMvc.perform(get("/users/profile"))
        .andExpect(status().isOk)
        .andExpect(content().contentType("text/html;charset=UTF-8"))
        .andExpect(content().string(containsString("用户资料"))) 
}
kotlin
@Test
fun `should return json response`() {
    mockMvc.perform(post("/api/users")
        .contentType(MediaType.APPLICATION_JSON)
        .content("""{"name":"李四","email":"[email protected]"}"""))
        .andExpect(status().isCreated)
        .andExpect(jsonPath("$.id").exists()) 
        .andExpect(jsonPath("$.name").value("李四"))
}

端到端测试:完整的真实体验

🌐 什么是端到端测试?

端到端测试使用真实的 HTTP 客户端和运行中的服务器,模拟用户的完整操作流程。

🚀 Spring Boot 端到端测试

kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserIntegrationTest {
    
    @Autowired
    private lateinit var testRestTemplate: TestRestTemplate
    
    @Autowired
    private lateinit var userRepository: UserRepository
    
    @Test
    fun `should create user end to end`() {
        // 准备测试数据
        val createUserRequest = CreateUserRequest("王五", "[email protected]")
        
        // 发起真实 HTTP 请求
        val response = testRestTemplate.postForEntity( 
            "/api/users",
            createUserRequest,
            User::class.java
        )
        
        // 验证 HTTP 响应
        assertThat(response.statusCode).isEqualTo(HttpStatus.CREATED)
        assertThat(response.body?.name).isEqualTo("王五")
        
        // 验证数据库状态
        val savedUser = userRepository.findByEmail("[email protected]") 
        assertThat(savedUser).isNotNull
        assertThat(savedUser?.name).isEqualTo("王五")
    }
}

对比分析:选择合适的测试策略

📊 特性对比表

特性MockMvc端到端测试
运行速度⚡ 快速🐌 相对较慢
资源消耗💚 低🔴 高
真实性🟡 部分真实✅ 完全真实
调试难度💚 容易🟡 中等
外部依赖❌ 需要模拟✅ 真实调用
数据库操作❌ 需要模拟✅ 真实操作

🎯 使用场景指南

MockMvc 适用场景

  • Web 层逻辑测试:验证请求路由、参数绑定、响应格式
  • 控制器单元测试:专注业务逻辑,隔离外部依赖
  • 快速反馈循环:开发阶段的快速验证
  • API 契约测试:验证接口规范

端到端测试适用场景

  • 完整业务流程验证:用户注册、订单处理等复杂流程
  • 集成测试:验证各组件协作
  • 回归测试:确保新功能不破坏现有功能
  • 部署前验证:确保系统在真实环境中正常工作

实战:构建测试金字塔

🏗️ 测试策略设计

💼 实际项目中的测试组合

kotlin
@ExtendWith(MockitoExtension::class)
class UserServiceTest {
    
    @Mock
    private lateinit var userRepository: UserRepository
    
    @InjectMocks
    private lateinit var userService: UserService
    
    @Test
    fun `should throw exception when user not found`() {
        // 专注测试业务逻辑
        `when`(userRepository.findById(999L)).thenReturn(null)
        
        assertThrows<UserNotFoundException> {
            userService.findById(999L) 
        }
    }
}
kotlin
@WebMvcTest(UserController::class)
class UserControllerTest {
    
    @Test
    fun `should handle validation errors`() {
        // 测试 Web 层的参数验证
        mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""{"name":"","email":"invalid-email"}""")) 
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.errors").isArray) 
    }
}
kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserRegistrationFlowTest {
    
    @Test
    fun `should complete user registration flow`() {
        // 1. 注册用户
        val registerResponse = testRestTemplate.postForEntity(
            "/api/auth/register",
            RegisterRequest("新用户", "[email protected]", "password123"),
            User::class.java
        )
        
        // 2. 验证邮箱(模拟)
        val userId = registerResponse.body?.id
        testRestTemplate.postForEntity(
            "/api/auth/verify-email",
            VerifyEmailRequest(userId!!, "verification-token"),
            Void::class.java
        )
        
        // 3. 登录
        val loginResponse = testRestTemplate.postForEntity(
            "/api/auth/login",
            LoginRequest("[email protected]", "password123"),
            LoginResponse::class.java
        )
        
        // 4. 验证完整流程
        assertThat(loginResponse.statusCode).isEqualTo(HttpStatus.OK) 
        assertThat(loginResponse.body?.token).isNotEmpty()
    }
}

最佳实践与建议

✨ MockMvc 最佳实践

MockMvc 使用技巧

  1. 专注 Web 层:不要试图测试业务逻辑,那是单元测试的职责
  2. 合理使用 @MockBean:只模拟直接依赖,避免过度模拟
  3. 验证关键点:状态码、响应格式、错误处理
  4. 利用服务端优势:检查异常处理、模型数据等内部状态

🎯 端到端测试最佳实践

端到端测试技巧

  1. 选择关键路径:不是所有功能都需要端到端测试
  2. 数据管理:使用测试数据库或事务回滚
  3. 环境隔离:确保测试环境的独立性
  4. 性能考虑:端到端测试较慢,合理控制数量

⚠️ 常见陷阱

避免这些错误

  • 过度依赖 MockMvc:不能替代真实的集成测试
  • 忽视测试边界:MockMvc 测试中混入过多业务逻辑测试
  • 端到端测试过多:导致测试套件运行缓慢
  • 缺乏测试策略:没有明确的测试分层

总结:构建高效的测试体系

MockMvc 和端到端测试各有其价值和适用场景:

  • MockMvc 像是一个精密的显微镜,让我们能够清晰地观察 Web 层的每个细节
  • 端到端测试 像是一个望远镜,让我们能够看到整个系统的全貌

IMPORTANT

最佳的测试策略不是选择其中一种,而是根据项目需求和团队情况,合理组合使用,构建一个既高效又可靠的测试金字塔。

记住:好的测试不仅能发现问题,更能给开发者信心去重构和改进代码 🚀