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 的角色边界。

Fake vs Mock 核心区别框架图 Fake vs Mock 核心区别——三层对比框架 Fake(伪造) 轻量级工作实现,具备真正内部状态 例:内存数据库、内存文件系统 Mock(模拟) 预编程行为 + 交互验证替身 例:mock HTTP 客户端、mock 消息队列 行为特征 侧重心智模型:模拟真实逻辑 状态可观察、可断言 行为特征 侧重交互记录:方法是否被调用 验证参数、次数、顺序 状态验证 断言 fakeObject.getResult() == 预期 行为验证 verify(mockObj).doSomething(param) 选择 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 总结:选择策略

  1. 优先用 Fake 当依赖可以且容易实现轻量级替代(如内存数据库、文件系统模拟),并且测试需要验证业务逻辑的正确性。
  2. 优先用 Mock 当测试重点是外部协作者的交互协议(如审计日志记录、消息发送),或者依赖难以/没必要真实实现(如第三方支付接口)。
  3. 混用原则:一个测试夹具中可以同时包含 Fake(用于基础设施层)和 Mock(用于边界接口),但功能上不要重叠。