第3章 什么是Fake:轻量级替代实现

3.1 核心概念:功能完整但简化的替代品

Fake 是一种测试夹具(Test Fixture),它提供一个轻量级但功能完整的替代实现,用于替换真实组件(如数据库、文件系统、网络服务)。与真实组件不同,Fake不依赖外部基础设施,通常运行在内存中,速度更快、行为可控,且能模拟真实组件的核心业务逻辑。

关键特征:Fake 实现了与真实组件相同的接口,但内部采用简化的算法或存储方式。例如一个内存中的数据库 Fake 支持增删改查,但数据不持久化到磁盘,也不依赖数据库引擎。

3.2 原理图解:Fake 在测试中的位置

下面的 SVG 展示了 Fake 与真实组件、Stub 的关系。Fake 位于测试代码与被测系统之间,作为一个完整的替代层运行。

Fake 原理结构图 Fake 在测试中的角色 测试代码 被测系统 (SUT) Fake (完整功能替代) (内存数据库/伪服务) Stub (预设返回值) (无业务逻辑) 真实数据库 / 外部服务 Fake 内部逻辑完整, 可验证状态变更。

3.3 类比:替代方案的理解

将 Fake 类比为“模拟飞行器”:真实飞行需要真实飞机、机场和空域(真实组件)。模拟飞行器在普通电脑上运行,提供完整的飞行物理引擎和仪表盘,学员可以进行起降、导航等练习,所有操作逻辑与真实飞行一致,但不会实际离开地面。这就是 Fake——功能完整、逻辑真实,但运行在简化环境中。

概念 真实组件 Fake Stub
功能完整性 完全 完整,但简化实现 不完整,仅返回固定值
业务逻辑 真实业务逻辑 有真实逻辑,使用轻量级算法 无逻辑,硬编码响应
状态管理 持久化状态 内存中状态,可查询/修改 无状态,每次调用返回预设值
外部依赖 依赖基础设施 完全隔离,无外部依赖 无外部依赖
适用场景 生产集成测试 单元/集成测试需验证交互 替代难以实例化的依赖

3.4 Fake 与 Stub 的本质差异

许多人混淆 Fake 和 Stub,两者最核心的区别在于“是否具有真实的业务逻辑”:

  • Stub 只负责返回预设数据,不关心输入参数的具体含义,内部无任何算法。例如一个获取用户信息的 Stub 永远返回{"name":"张三"}
  • Fake 则根据输入参数执行真实逻辑,例如一个用户信息 Fake 会根据传入的 ID 从内部 Map 中查找对应数据,若不存在则返回空,类似于真实数据库的行为。

常见误区:“Fake 就是复杂一点的 Stub。” 错!Fake 拥有完整的接口实现逻辑,可以测试数据的增删改查流程;而 Stub 只能测试被测系统是否调用了依赖方法。二者设计目的完全不同。

3.5 代码示例:Fake vs Stub 对比

以下 Python 示例清晰展示二者的区别(仅用于辅助理解,不必深入细节):

# --- Stub: 无逻辑,硬编码返回值 ---
class StubUserRepo:
    def find_by_id(self, user_id):
        return {"name": "张三", "email": "zhangsan@example.com"}

# --- Fake: 具有完整业务逻辑,内部使用字典模拟数据库 ---
class FakeUserRepo:
    def __init__(self):
        self._store = {
            1: {"name": "张三", "email": "zhangsan@example.com"},
            2: {"name": "李四", "email": "lisi@example.com"}
        }
    def find_by_id(self, user_id):
        return self._store.get(user_id)  # 返回 None 若不存在
    def save(self, user):
        self._store[user["id"]] = user
    def delete(self, user_id):
        self._store.pop(user_id, None)

# 测试:Fake 支持完整的CRUD验证
def test_delete_user():
    repo = FakeUserRepo()
    repo.save({"id": 3, "name": "王五", "email": "ww@example.com"})
    repo.delete(3)
    assert repo.find_by_id(3) is None  # 真实逻辑验证删除效果

3.6 应用边界:何时应该使用 Fake

  1. 依赖具有复杂状态逻辑:如数据库、缓存、消息队列、文件系统。
  2. 测试需要验证副作用:例如检查数据是否成功插入、更新或删除。
  3. 环境受限:CI/CD 环境无法连接真实数据库或外部 API。
  4. 需要多次调用并产生可预测结果:Fake 内部状态可重置,保证测试隔离。

边界提醒:如果依赖的接口只用于返回一个值(如配置查询),Stub 足够;若需要验证数据写入、并发行为或流程完整性,则应选用 Fake。过度使用 Fake 会导致测试维护成本上升,因为需要维护一套额外的业务逻辑副本。

3.7 常见误区清单

误区 正解
Fake 是 Stub 的“升级版” 二者是不同角色,Stub 无逻辑,Fake 有逻辑
Fake 可以替代真实组件跑全量集成测试 Fake 是简化版,可能遗漏真实组件的边界行为
Fake 必须与真实组件实现完全相同的接口 接口须一致,但内部实现可简化(如同步变异步需注意)
Fake 总是比 Mock 慢 Fake 常比 Mock 快(无框架开销),但维护成本高

3.8 练习题

  1. 场景判断:你要测试一个订单服务,它需要从用户服务获取用户等级然后计算折扣。用户服务返回数据量大且业务规则复杂(VIP、普通、黑名单等)。你会选择 Stub 还是 Fake?为什么?
  2. 差异填空:Stub 关注的是______,Fake 关注的是______。
  3. 设计挑战:如何用一个 FileSystemFake 来测试文件上传功能?描述需要实现哪些接口(如 exists()、read()、write()、delete()),并说明内部存储结构(例如使用字典 {路径: 内容})。

参考答案提示:第1题选择Fake,因为折扣计算依赖于用户等级的实际逻辑,需要返回不同用户的数据进行分支覆盖;第2题:Stub关注“是否调用”,Fake关注“调用是否正确执行了业务规则”。第3题需要模拟磁盘操作,在内存中维护路径与字节数据的映射。