Mock与Fake的核心区别:验证方式(期望 vs 结果)、实现代价、维护成本、适用阶段
虽然 Mock 和 Fake 都用来模拟依赖组件,但它们的设计目标和核心契约存在根本差异。理解这些差异是测试架构决策的关键:选错角色会导致测试脆弱、维护成本激增,甚至失去测试的有效性。以下从四个维度展开对比。
1. 验证方式:行为期望 vs 状态结果
这是最根本的区别。Mock 关注交互过程,Fake 关注最终结果。
- Mock(交互验证):验证被测试代码是否以期望的方式调用了依赖组件(例如:调用了某方法、传入了特定参数)。它通过设置期望(Expectations)和后续验证(Verify)来工作。
- Fake(状态验证):提供依赖组件的轻量级功能实现,测试只验证最终返回的结果或系统状态是否正确,完全不关心内部调用过程。
🎯 快速类比:
Mock = 收银员:收银员关注过程——你是否按正确步骤结账、是否出示了优惠券。一旦步骤错误,立刻干预。
Fake = 计算器:计算器只关注结果——你输入数字和运算符,它给出答案。它完全不关心你按按钮的顺序是否正确。
Mock = 收银员:收银员关注过程——你是否按正确步骤结账、是否出示了优惠券。一旦步骤错误,立刻干预。
Fake = 计算器:计算器只关注结果——你输入数字和运算符,它给出答案。它完全不关心你按按钮的顺序是否正确。
在代码层面,这不是直接依赖返回值,而是依赖行为记录:
// Mock 风格(交互验证)
mockService.doSomething("param");
verify(mockService).doSomething("param"); // 验证是否发生了此交互
// Fake 风格(状态验证)
fakeService.addData("key", "value");
assertEquals("value", fakeService.getData("key")); // 只验证结果状态
2. 实现代价
| 维度 | Mock | Fake |
|---|---|---|
| 实现复杂度 | 低——通常利用 Mock 框架(如 Mockito)动态生成 | 高——需要手写或维护一套轻量级的功能逻辑 |
| 代码量 | 极少——几行声明和验证语句 | 较多——需要实现接口的所有方法,包括边缘情况 |
| 初始化成本 | 极低——无状态,每次调用可即时生成 | 中等——需要预设数据或维护内部存储结构 |
结论:Mock 在编写阶段更"廉价",尤其适合快速隔离测试;Fake 前期建设成本更高,但具有长期价值。
3. 维护成本
这是被误解最深的维度。许多人认为 Mock 更易维护,但实际情况往往相反:
- Mock 维护成本高(脆弱性):Mock 与被测代码的行为强耦合。只要被测试代码修改了内部调用细节(即使外部行为不变),Mock 期望就会失效,导致测试红色失败。这种"脆弱测试"会严重消耗团队维护精力。
- Fake 维护成本低(健壮性):Fake 与接口契约耦合。只要接口签名和业务语义不变,即使内部调用逻辑重构,Fake 和测试代码都无需修改。它只关心"结果对不对",不关心"过程怎么走"。
⚠️ 常见误区: "使用 Mock 能减少维护工作"——错! 过度使用 Mock 进行行为验证会创建大量脆弱测试。每次代码重构(即使不改变外部行为)都需要同步更新 Mock 期望。相比之下,基于 Fake 的状态验证测试往往能通过重构而毫发无损。
4. 适用阶段
- Mock → 单元测试阶段:当需要完全隔离被测代码,验证其与其他组件之间的协作契约是否被严格遵守时,Mock 是唯一选择。典型场景:验证第三方 SDK 调用参数、验证回调是否触发。
- Fake → 集成测试/功能测试阶段:当需要验证数据流和业务逻辑是否正确,且不希望依赖真实基础设施(如数据库、外部 API)时,Fake 是最佳选择。典型场景:使用内存数据库 Fake 代替真实数据库运行全套仓储测试。
核心区别总结
以下 SVG 结构图直观展示了 Mock 与 Fake 在上述四个维度的对比:
常见误区
- "Mock 和 Fake 可以完全互换" —— 不可以。它们在验证哲学和适用阶段上有本质冲突,生硬互换将导致测试失去焦点或成本失控。
- "Fake 就是复杂的 Mock" —— Fake 是功能实现,不涉及行为验证;Mock 是交互记录器,不提供真实功能。两者概念平行,不存在包含关系。
- "使用 Mock 框架就不需要手写 Fake" —— Mock 框架只能生成 Mock,无法生成具备真实业务语义的 Fake。当你需要模拟数据库行为时,必须手写 Fake。
练习题
判断以下场景应使用 Mock 还是 Fake,并说明理由。
- 场景 A:测试一个支付网关包装器,需要验证是否向网关发送了正确的加密签名参数,且只关心签名参数是否正确,不关心网关是否真正处理了交易。
- 场景 B:测试一个用户仓储模块,需要确保其 CRUD 操作的正确性。你希望测试能独立于真实数据库运行,且只关心数据的存取结果是否正确。
💡 参考答案:
场景 A → Mock:核心关注点是调用参数,属于交互行为验证,必须使用 Mock。
场景 B → Fake:核心关注点是数据存取状态,且需要模拟完整功能,最适合手写一个基于 HashMap 的内存 Fake 实现。
场景 A → Mock:核心关注点是调用参数,属于交互行为验证,必须使用 Mock。
场景 B → Fake:核心关注点是数据存取状态,且需要模拟完整功能,最适合手写一个基于 HashMap 的内存 Fake 实现。
应用边界
- Mock 的边界:不要对简单数据容器或不涉及外部协作的内部查询使用 Mock。这会引入无意义的脆弱性。Mock 只用于验证跨组件边界的交互契约。
- Fake 的边界:不要为无状态纯函数或一次性计算逻辑构建 Fake。Fake 的维护成本在此时是多余的。Fake 只用于提供需要长期维护的、有状态的依赖替代品(如数据库、文件系统、第三方服务 SDK)。
- 混合使用策略:在同一项目中,两者可以共存。例如:在单元测试层使用 Mock 隔离外部依赖,在集成测试层使用 Fake 验证数据流。关键在于清晰区分各自的职责边界。