Fake与Mock的核心区别:定义、意图、适用场景、生命周期控制要点
在测试夹具(Test Fixture)设计中,Fake 和 Mock 是两种最常见的替身(Test Double)模式。尽管两者都用来替代真实依赖,但它们在定义、意图、适用场景和生命周期控制上存在本质差异。混淆它们会导致测试脆弱、维护成本激增。
核心原则:Fake 提供轻量级功能实现,侧重模拟行为的结果;Mock 则记录交互并验证调用序列与参数,侧重行为交互的检查。
1.1 定义对比
| 维度 | Fake(伪造) | Mock(模拟) |
|---|---|---|
| 本质 | 真实组件的简化/轻量级工作实现 | 预编程行为 + 交互验证的替身对象 |
| 示例 | 内存数据库替代真实 MySQL | 通过 mock 框架模拟的外部 HTTP 调用 |
| 副作用 | 通常带有真正的内部状态(如内存中写入数据) | 一般不维护实际状态,仅返回预设值 |
| 测试风格 | 状态验证:断言 Fake 内部状态是否符合预期 | 行为验证:断言是否按期望调用了方法/参数 |
1.2 意图差异
- Fake 意图:让测试快速通过,避免真实基础设施(如数据库、文件系统、网络)的启动成本与不确定性。Fake 本身包含业务逻辑的简化版,通常需要单独维护。
- Mock 意图:验证被测单元与其协作者之间的交互协议——方法是否被调用、调用次数、参数是否匹配、调用顺序是否正确。Mock 不关心最终状态,只关心交互过程。
1.3 适用场景速查
| 场景 | 推荐类型 | 理由 |
|---|---|---|
| 需要依赖的真实行为(如 SQL 查询结果排序) | Fake | Fake 可模拟简单逻辑,Mock 返回固定值无法验证行为正确性 |
| 验证对外部接口的调用协议(如参数格式、超时处理) | Mock | Mock 提供精确的调用验证,Fake 不记录调用细节 |
| 集成测试中替换慢/不稳定的外部服务 | Fake | Fake 在内存运行,速度更快,且不依赖网络 |
| 单元测试中验证边界条件(如异常抛出后重试逻辑) | Mock | Mock 可轻松模拟异常场景并验证重试次数 |
| 无状态的数据转换(如哈希计算、格式化) | Fake | Fake 提供与真实实现一致的计算结果,无需验证调用 |
1.4 生命周期控制要点
Fake 与 Mock 在创建时机、状态重置、验证时机上存在关键差异,直接影响测试夹具的组织方法。
- Fake 生命周期:
- 通常在
@BeforeEach/setUp()中创建并初始化状态。 - 每个测试用例可共享同一个 Fake 实例(需注意状态隔离),或每次创建新实例。
- 测试结束后无需显式销毁,但若 Fake 持有线程/文件句柄,需在
@AfterEach中关闭。
- 通常在
- Mock 生命周期:
- 必须在每个测试方法内(或
@BeforeEach中)创建 Mock 对象,并通过框架进行“录制-回放-验证”。 - 严格隔离:不同测试用例不能共享 Mock 实例,否则交互记录会混叠。
- 验证必须在测试方法结束前显式调用
verify()(Mockito)、assertExpectations()(EasyMock)等 API。
- 必须在每个测试方法内(或
夹具设计建议:Fake 可放入共享测试夹具基类以提高复用;Mock 建议在每个测试方法内部创建,避免隐式状态泄漏。
1.5 核心区别框架图
下图从定义层、行为层、验证层三个维度直观展示 Fake 与 Mock 的角色边界。
1.6 代码示例:Fake 与 Mock 的直观差异
以下代码演示同一功能(用户保存)的 Fake 与 Mock 实现方式。
// ==== Fake 风格 ====
class UserRepositoryFake : UserRepository {
private val storage = mutableMapOf<String, User>()
override fun save(user: User) {
storage[user.id] = user
}
override fun findById(id: String): User? = storage[id]
}
fun testFake() {
val repo = UserRepositoryFake()
repo.save(User("1", "Alice"))
val saved = repo.findById("1")
// 状态验证:断言 Fake 的内部状态
assert(saved?.name == "Alice")
}
// ==== Mock 风格(使用 Mockito) ====
fun testMock() {
val mockRepo = mock<UserRepository>()
`when`(mockRepo.findById("1")).thenReturn(User("1", "Alice"))
// 被测代码内部调用了 mockRepo.save(user)
val service = UserService(mockRepo)
service.register("1", "Alice")
// 行为验证:断言方法被按预期调用
verify(mockRepo).save(any())
}
常见错误:不要对 Fake 使用行为验证(Fake 不记录调用细节);也不要对 Mock 使用状态断言(Mock 无真实状态)。混淆两者会导致测试既慢又不可靠。
1.7 总结:选择策略
- 优先用 Fake 当依赖可以且容易实现轻量级替代(如内存数据库、文件系统模拟),并且测试需要验证业务逻辑的正确性。
- 优先用 Mock 当测试重点是外部协作者的交互协议(如审计日志记录、消息发送),或者依赖难以/没必要真实实现(如第三方支付接口)。
- 混用原则:一个测试夹具中可以同时包含 Fake(用于基础设施层)和 Mock(用于边界接口),但功能上不要重叠。