Mock 不是万能替身:Dummy / Stub / Spy / Mock / Fake 五种 Test Double 的边界
一、五个词的分工,其实就一句话
Test Double 的分类(Meszaros 的经典划分)在中文语境里常被笼统说成”打桩”,但五种东西的职责完全不同:
| 类型 | 一句话职责 | 断言对象 |
|---|---|---|
| Dummy | 只为填参数存在,测试里不会被用到 | 不断言 |
| Stub | 返回预先准备好的固定值,让流程走通 | 被测方法的返回值 |
| Spy | 记录被调用情况,测试结束后再检查 | 被测方法的返回值 + 少量交互 |
| Mock | 预先声明”必须以某种方式被调用”,不满足即失败 | 交互本身(调用次数、参数、顺序) |
| Fake | 一个真的、能跑的简化实现(内存仓储、假网关) | 走真实逻辑的结果 |
那张表里最有用的一列是”断言对象”。社区文章的核心观点我们完全认同:先明确你要断言什么,再选替身——而不是反过来,因为手上有个 Mock 框架就把所有依赖都 Mock 掉。

二、滥用 Mock 会付出什么代价
Mock 的问题是它把实现细节写进了测试。
// 断言"调用了 3 次数据库查询" —— 这就是把实现钉死 |
一旦有人优化了查询方式,从三次合并成一次——功能更好了,测试却红了。这类”改动明明是对的,测试却在阻止你”的体验,是所有团队最终放弃维护单测的起点。我们在给客户做遗留系统改造时,接手过测试通过率 90%、但覆盖率只有 15% 的项目,翻开细看几乎全是这种交互断言。
判断标准其实很简单:如果这个断言在业务人员看来没有意义(”客户只关心订单能不能下,不关心中间查了几次库”),那它就不该是 Mock 断言。
三、我们的选型顺序(从便宜到贵)
写测试时按这个顺序往下走,能停就停:
- 能用 Dummy 就别写别的——参数不能为 nil,但你根本不关心它,传个空对象。
- 只关心”给定输入返回什么”,用 Stub。 绝大多数纯逻辑分支测试走这条。
- 依赖行为本身不重要、但需要一个能跑的存储,用 Fake。 内存版 repository、SQLite 内存库都属此类。这是我们最推荐、也最少被使用的一档。 Fake 让测试走真实逻辑,重构时不会误报。
- 要看”有没有被调用”(比如审计事件、外部通知),用 Spy。 注意:只记录,不预设次数。
- 只有当”交互契约本身就是需求”时才用 Mock。 例如:调用第三方支付必须幂等重试不超过 3 次、下单必须发短信——这些是业务承诺,不是实现细节,才值得用 Mock 断言。
四、什么时候”别打桩”
有三种情况我们建议直接测真实依赖:
- 数据库交互。 内存 Fake 与真库在事务边界、唯一约束、日期与时区行为上并不等价。涉及这些的仓储层,我们宁可跑一个容器里的真库,也不要一个永远绿的假测试。
- 序列化 / JSON 契约。 Mock 掉编解码等于测不到线上最常见的那类故障。
- 框架自身的中间件链。 这类问题只在真实请求生命周期里暴露。
一句话总结:替身是为了隔离不确定性,不是为了省启动时间。 如果依赖本身行为稳定、又恰好是你想验证的东西,打桩只是在制造盲区。
五、给正在写 AI 应用测试的团队补一句
我们今年一半以上的项目是大模型应用。这里有个新问题:模型输出天然不可复现,把 LLM 客户端 Mock 掉是最省事的做法,但它同时把”提示词工程是否有效”这件事排除在测试之外了。
我们的折中是三层:
- 契约层用 Stub:固定返回一段已知文本,测解析、重试、超时——这部分必须是确定性的;
- 效果层用真实小样本:几十条标注样例跑离线评估,不放进每次 CI,而是每天定时跑;
- 红线层用 Spy:断言”敏感字段绝没有出现在送出模型的 prompt 里”——这类是安全承诺,值得钉死。
分层之后,测试既不会因为模型抖动而随机失败,也不会因为全 Mock 而什么都没测到。
五种 Test Double 的职责划分沿用 Meszaros 经典分类;选型顺序与 AI 应用测试分层为洹拓科技交付实践。
新乡洹拓科技有限公司,软件开发 · AI模型研发 · 云服务与部署运维。
咨询热线:400-829-9957
地址:河南省新乡市辉县市金城百货四楼