3、Mock核心概念:什么是Mock、Stub与Fake的区别、Mock在测试金字塔中的位置、Mock对象生命周期
在微服务架构中,服务间依赖错综复杂。为了对单个服务进行独立、可重复的验证,必须引入测试替身(Test Double)技术。本章将深入剖析Mock的核心概念,厘清其与Stub、Fake的边界,并明确其在测试策略中的定位与生命周期。
3.1 什么是Mock:行为验证的替身
Mock对象是一种测试替身,它预先注册了期望的调用(Expectation),并在测试执行后验证这些调用是否确实发生。与Stub不同,Mock的核心关注点是行为验证(Behavior Verification),而非状态验证。
核心定义:Mock是一个“智能”的替身对象,它记录所有被调用的方法及参数,并在测试结束时断言这些调用是否符合预设的期望。
典型的Mock工作流程包含三个步骤:
- 设置期望(Setup Expectation):定义“应该被调用”的方法、参数及返回值。
- 执行被测代码(Exercise):运行待测单元,使其与Mock交互。
- 验证交互(Verify):断言Mock上的所有期望调用均已按预期发生。
// 伪代码示例:使用Mock验证支付服务被调用
// 1. 设置期望
mockPaymentService.expects(once())
.method("charge")
.with(eq(100.0), eq("USD"))
.willReturn(true);
// 2. 执行被测代码
OrderService orderService = new OrderService(mockPaymentService);
orderService.placeOrder(100.0, "USD");
// 3. 验证交互
mockPaymentService.verify(); // 验证charge方法是否被调用一次
3.2 Stub与Fake的区别:三种替身的对比
理解Mock、Stub与Fake的区别是正确使用测试替身的前提。下表从多个维度进行了对比:
| 维度 | Mock | Stub | Fake |
|---|---|---|---|
| 核心目标 | 行为验证(交互是否发生) | 状态验证(返回预设数据) | 轻量级功能实现(简化逻辑) |
| 验证方式 | 测试结束后显式调用 verify() | 不验证,仅提供数据 | 不验证,运行真实但简化的逻辑 |
| 是否包含逻辑 | 否,仅记录调用 | 否,返回固定值 | 是,包含可工作的简化实现 |
| 典型场景 | 验证外部服务调用、消息发送 | 模拟数据库查询返回结果 | 内存数据库替代真实数据库 |
| 复杂度 | 中(需管理期望) | 低(仅返回数据) | 高(需实现简化逻辑) |
经验法则:如果你关心“是否调用了某个方法”,使用Mock;如果你关心“调用后返回了什么数据”,使用Stub;如果你需要一个可工作的轻量级替代品(如内存数据库),使用Fake。
3.3 Mock在测试金字塔中的位置
测试金字塔(Test Pyramid)定义了不同粒度的测试策略。Mock主要位于单元测试和集成测试的边界处,用于隔离被测单元的外部依赖。
下图展示了Mock在测试金字塔中的典型位置:
常见误区:过度使用Mock会导致测试与实现细节过度耦合,使测试变得脆弱。应优先使用Fake(如内存数据库)替代真实依赖,仅在需要验证交互行为时才使用Mock。
3.4 Mock对象生命周期
一个Mock对象从创建到销毁,通常经历以下四个阶段:
- 创建(Creation):使用Mock框架(如Mockito、EasyMock)动态生成代理对象。
- 录制(Recording):定义期望的调用行为(方法、参数、返回值、异常)。
- 回放(Replaying):被测代码执行期间,Mock对象根据录制规则返回预设值或执行预设动作。
- 验证(Verification):测试结束时,验证所有期望的调用是否按预期发生。
下图展示了Mock对象的完整生命周期:
最佳实践:在JUnit中使用 @Mock 注解创建Mock,配合 MockitoAnnotations.openMocks(this) 在 @Before 中初始化,并在 @After 中调用 Mockito.validateMockitoUsage() 确保所有Mock均已验证。
3.5 本章小结
- Mock 是行为验证的测试替身,关注“是否被调用”。
- Stub 提供预设数据,关注“返回什么”。
- Fake 是轻量级功能实现,关注“如何工作”。
- Mock在测试金字塔中位于单元测试与集成测试的隔离层。
- Mock生命周期包含创建 → 录制 → 回放 → 验证四个阶段,每个测试用例应独立管理。