第3章 什么是Fake:轻量级替代实现
3.1 核心概念:功能完整但简化的替代品
Fake 是一种测试夹具(Test Fixture),它提供一个轻量级但功能完整的替代实现,用于替换真实组件(如数据库、文件系统、网络服务)。与真实组件不同,Fake不依赖外部基础设施,通常运行在内存中,速度更快、行为可控,且能模拟真实组件的核心业务逻辑。
关键特征:Fake 实现了与真实组件相同的接口,但内部采用简化的算法或存储方式。例如一个内存中的数据库 Fake 支持增删改查,但数据不持久化到磁盘,也不依赖数据库引擎。
3.2 原理图解:Fake 在测试中的位置
下面的 SVG 展示了 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
- 依赖具有复杂状态逻辑:如数据库、缓存、消息队列、文件系统。
- 测试需要验证副作用:例如检查数据是否成功插入、更新或删除。
- 环境受限:CI/CD 环境无法连接真实数据库或外部 API。
- 需要多次调用并产生可预测结果:Fake 内部状态可重置,保证测试隔离。
边界提醒:如果依赖的接口只用于返回一个值(如配置查询),Stub 足够;若需要验证数据写入、并发行为或流程完整性,则应选用 Fake。过度使用 Fake 会导致测试维护成本上升,因为需要维护一套额外的业务逻辑副本。
3.7 常见误区清单
| 误区 | 正解 |
|---|---|
| Fake 是 Stub 的“升级版” | 二者是不同角色,Stub 无逻辑,Fake 有逻辑 |
| Fake 可以替代真实组件跑全量集成测试 | Fake 是简化版,可能遗漏真实组件的边界行为 |
| Fake 必须与真实组件实现完全相同的接口 | 接口须一致,但内部实现可简化(如同步变异步需注意) |
| Fake 总是比 Mock 慢 | Fake 常比 Mock 快(无框架开销),但维护成本高 |
3.8 练习题
- 场景判断:你要测试一个订单服务,它需要从用户服务获取用户等级然后计算折扣。用户服务返回数据量大且业务规则复杂(VIP、普通、黑名单等)。你会选择 Stub 还是 Fake?为什么?
- 差异填空:Stub 关注的是______,Fake 关注的是______。
- 设计挑战:如何用一个 FileSystemFake 来测试文件上传功能?描述需要实现哪些接口(如 exists()、read()、write()、delete()),并说明内部存储结构(例如使用字典 {路径: 内容})。
参考答案提示:第1题选择Fake,因为折扣计算依赖于用户等级的实际逻辑,需要返回不同用户的数据进行分支覆盖;第2题:Stub关注“是否调用”,Fake关注“调用是否正确执行了业务规则”。第3题需要模拟磁盘操作,在内存中维护路径与字节数据的映射。