编写测试代码时,总会遇到一道难关。
“这是 Mock 还是 Stub?”让人困惑的时刻。
明明都像“假对象”,但代码评审时听到“那不是 Mock,是 Stub”就会大脑一片空白。
本文将结合示例,清晰梳理测试替身五兄弟(Dummy、Stub、Spy、Mock、Fake)的区别。
先说结论。
测试替身取决于“验证什么”。返回值的是 Stub,记录调用的是 Spy,判断是否为预期调用的是 Mock,像真实对象一样运行的是 Fake。
记住这句话,就完成一半了。
测试替身到底是什么?
测试替身(Test Double)一词源自电影中的“特技替身(stunt double)”。
就像替身代替演员完成危险场面,它泛指代替真实对象的假对象。
Gerard Meszaros 在《xUnit Test Patterns》(2007)中系统整理了这一概念。
所以 Mock、Stub 都是测试替身的子类型。
为什么要用?例如测试支付逻辑时,不可能真的调用银行卡公司的 API。
速度慢、会产生费用,网络中断还会导致测试失败。
因此要设置一个替身来代替真实对象。
Mock、Stub、Spy、Fake 的区别一表总结
用文字解释容易混淆,先看表格。
| 类型 | 核心作用 | 验证对象 |
|---|---|---|
| Dummy | 只占位 | 不验证 |
| Stub | 返回固定值 | 状态(结果值) |
| Spy | 记录调用记录 | 状态+调用记录 |
| Mock | 判断是否为预期调用 | 行为(调用) |
| Fake | 轻量地像真实对象一样运行 | 状态(结果值) |
这里最重要的区分只有一个。
是验证状态,还是验证行为?
Stub 和 Fake 看“结果是否如此”(状态验证)。
Mock 看“该方法是否真的被调用”(行为验证)。
Spy 处于两者之间,负责悄悄记录调用。
看代码就很直观
代码比文字更容易理解。以用户通知功能为例。在 Swift 中,通常会直接创建遵循协议的测试替身。
先看 Stub:只返回固定值的替身。
// getName固定为始终返回“洪吉童”
final class UserRepositoryStub: UserRepository {
func getName(id: Int) -> String { "洪吉童" }
}
let service = GreetingService(repository: UserRepositoryStub())
// 测试只确认结果值(状态)是否正确
#expect(service.greet(id: 1) == "洪吉童")
接下来是 Mock,用来验证“这个方法是否被调用”。
// 验证通知是否真的发送(被调用)
final class NotificationSenderMock: NotificationSender {
var sendCallCount = 0
func send(_ message: String) { sendCallCount += 1 }
}
let mockSender = NotificationSenderMock()
let service = UserService(sender: mockSender)
service.notifyUser(id: 1)
// 行为验证: send是否准确调用了 1次
#expect(mockSender.sendCallCount == 1)
看出区别了吗?Stub 检查返回值(结果),Mock 检查调用次数。
这种视角差异是理解测试替身的关键。
那么 Spy 和 Fake 什么时候用?
Spy会包装真实对象,保留真实行为,同时偷偷记录调用记录。
当你想保留“真实逻辑”,又想知道调用了几次时,就使用它。
在 Swift 中,常见做法是让 Spy 类委托给真实实现,并把调用参数和次数记录到属性中。
Fake的行为类似真实对象,但实现轻量得多。
常见例子是替代真实数据库的内存存储,或使用 Dictionary 创建的假仓储。
它的行为像真实对象,但不足以用于生产环境,就是专门用于测试的实现。
总结如下。
- 只需要值 → Stub
- 需要验证是否被调用 → Mock
- 既要真实行为又要调用记录 → Spy
- 需要像真实对象一样运行的轻量实现 → Fake
常见问题
Q. Mock 和 Stub 在实际工作中可以混用吗?
实际工作中,一个手写测试替身类常常同时承担返回值(Stub)和记录调用(Mock)的职责,因此边界比较模糊。不过仍应区分目的是“提供值”还是“验证调用”,这样测试意图才清晰。
Q. 听说 Mock 用太多不好?
滥用行为验证会导致内部实现稍有变化,测试就成批失败。因此建议尽量以状态验证(Stub、Fake)为主,只在必要的协作关系中使用 Mock。
与其死记五种类型,不如问自己:“现在需要值,还是要验证调用?”
只要回答这一个问题,就能自然决定该创建哪种替身。
今天写测试代码又感到困惑时,再打开这篇文章看看吧。祝你的测试更加可靠!

