本节摘要:把外部依赖"关进门"之后,用什么假对象去顶它们,决定测试离真相有多远。替身分四族——Stub 顶返回值、Mock 验行为、Spy 记录调用、Fake 提供简化实现。本节给一张选择流程和四段可运行代码,终结"替身混着用"的糊涂账。
Mock、Stub、Spy、Fake 这些词在团队里几乎被用成一锅粥,人人叫"打 Mock",实际干的事却大不相同。之所以乱,是因为它们都长得像:一个帮你摆脱真实依赖的假对象。区别不在长相,在你拿它干哪件事。搞不清这点,最常见的后果是拿 Stub 当 Mock 用,断言一堆根本没人调用的"假交互",让测试越写越紧、越死。
| 替身 | 干什么 | 你期待的回报 | 典型场景 |
|---|---|---|---|
| Stub | 固定返回预设值 | 一份数据 | 顶替读接口,省得连库 |
| Mock | 验证方法被正确调用过 | 一条交互断言 | 确认扣款接口被调到 |
| Spy | 记录真实调用,事后查 | 一份调用日志 | 包装真对象观察其行为 |
| Fake | 提供简化但真的实现 | 一块能实际工作的替代 | 用内存表代替真数据库 |
一句话记住家族关系:Stub 给"数据",Mock 断"行为",Spy 记"过程",Fake 是"缩小头的真身"。Mock 和 Spy 最常被弄混——Mock 是你要验证它被人调了,Spy 是你要看它到底被怎么调的而不打算造假。
下面把四族画进一张选择流程,方便你跺几脚就能落定。

下面用 Python 的 unittest.mock 把四族各跑一遍,你盯着关键差异看。
Stub——固定返回,不关心调用:
from unittest.mock import Mock rate = Mock(return_value=0.1) # Stub 风格:永远回 0.1 assert rate("gold") == 0.1
Mock——确认有人调用过:
billing = Mock() charge(user_id=7, amount=9.99, billing=billing) billing.charge.assert_called_once_with(user_id=7, amount=9.99) # 验证行为
Spy——包真实对象,看它被怎么用:
real = RealLogger() spy = Mock(wraps=real) # Spy 风格:真实执行但留记录 spy.log("hi") spy.log.assert_called_once_with("hi")
Fake——小型真实实现,替代数据库:
class FakeDB: def __init__(self): self.rows = {} def put(self, k, v): self.rows[k] = v def get(self, k): return self.rows.get(k)
Fake 最值得一提的是它不"造假行为",它是一块能真工作的迷你实现,所以它往往最能反映真实集成行为——只要你保证它的口径和生产一致。
替身有两个极端。一端是过度替身:连第三方 SDK 的整棵对象树都想 Mock,替身比真对象还难维护,测得其实是"测试自己编的假世界"。另一端是从不给替身:单元测试里硬连数据库,等于放弃单元层。正确的姿势是按 2.2 节的账算——快、稳、贵该换的就换,能真用的就用 Fake,想在行为层立契约的就用 Mock 或 Spy。替身的价值不在"假",而在让你能把被测逻辑孤立出来,看清我自己而非外部世界的脸色。
一说替身,人容易想到"全是假的,测不到真相"。这个担忧有解:一是多倾斜向 Fake——它是一块能真工作的迷你实现,比纯粹的 Mock 更接近真身;另一方面是给替身配一层"真实体积"——比如替身读的格式、字段和真实实现保持一致,让替身的行为口径尽量和生产对齐。更进一步,把"我以为外部长什么样"写成契约文件(对应第 3 章的契约测试),让替身永远和真实约定的接口同步,就不会出现"替身记得一套、外部早已改了"的错位。
给四族排个气质轴,能帮你一眼落定。Stub 是"确定性"的——我要它回什么它回什么,我关心出参;Mock 是"验证性"的——我要它证明某方法被调对了几次、带什么参数;Spy 介于两者——它让真实现执行但你事后查看记录;Fake 是"实现性"的——它是真身缩小版,有自己的状态和行为。大多数混淆源于"我到底是要确定出参,还是要验证行为"没想清。想清这层,再翻对应小节的代码,落定就快了。
用替身时最经典的错误之一,是在替身上做"恒真断言"。比如 Mock 了一个永远 return True 的护栏,再断言"护栏返回 True 时放行",这等于自己和自己对答案,测不出任何真实规则。要避开它,就把 Mock 的返回值设计成"多种真实场景"——True 放行测一条、False 拦截测一条、抛异常测一条,让替身成为可枚举的开关,而不是恒真的死水。这样替身才是在帮你测被测逻辑的分支,而不只是陪衬。
| 我的目的 | 首选 | 理由 |
|---|---|---|
| 只要一份固定返回数据 | Stub | 不关心是否被调,只关心出参 |
| 要证明某方法被调用过 | Mock | 验证交互与参数 |
| 想看真实对象怎么被执行 | Spy | 真执行 + 事后查记录 |
| 需要一个能真用的小型替代 | Fake | 最接近真实行为 |
| 验证替身口径与外部一致 | 配契约测试 | 让替身永远不"落伍" |