Mock与Stub的核心区别:验证行为 vs 提供数据
说实话,刚接触测试替身那会儿,我也搞不清 Mock 和 Stub 到底有什么不同。当时写了个单元测试,用 Mock 对象把结果硬编码返回,然后写了一大堆 assert_called_once —— 后来被 code review 怼了:「你这是在测实现,不是测行为。」
嗯,那次之后我才真正理解了:Mock 和 Stub 虽然长得像,但它们的核心职责完全不同。一个盯着「你怎么调用的」,另一个只管「你调用我给你啥」。
从定义看区别:行为 vs 数据
咱们先看一个最直白的对比表格,你大概扫一眼就能抓住核心。
| 维度 | Mock | Stub |
|---|---|---|
| 核心关注点 | 验证交互行为(是否被调用、调用次数、参数) | 提供预设返回数据(屏蔽外部依赖) |
| 会不会让测试失败 | 会 —— 如果期望的调用没发生,测试就挂 | 不会 —— 即使从未调用,测试照样通过 |
| 典型场景 | 验证支付服务是否调用了发送通知的接口 | 让数据库查询直接返回一批测试数据 |
| 实现方式 | 需要记录调用历史 + 支持断言 | 只需要覆盖方法返回值,不需要「记忆」 |
说白了,Stub 就像个工具人,你吩咐它「每次问张三的余额,都返回 100」,它照做就行,不关心张三存不存在。Mock 则像个监视器,它不光返回数据,还把你每次怎么调用的都记录下来,然后告诉你「不对,你应该先查余额再扣款,你顺序错了」。
使用场景决定选择
我个人习惯是:如果测试要验证「某个方法有没有被正确调用」,我就用 Mock;如果只是想跳过真实依赖(比如网络、数据库)的干扰,用 Stub 就够了。
举个例子,测试一个订单状态更新的功能:
- 用 Stub —— 让仓储接口直接返回「订单存在且状态为已支付」,测试业务逻辑的分支。
- 用 Mock —— 验证当订单超时时,是否确实调用了「发送催付短信」的方法,并且传参正确。
你看,两种替身解决的痛点完全不同。
验证行为:Mock 的职责
Mock 的核心能力是「记性好」。它会在内部维护一个调用记录表,你可以在测试最后检查:
# Python 示例(使用 unittest.mock)
from unittest.mock import Mock
sms_service = Mock()
order = Order(sms_service)
order.mark_as_paid()
# 验证 sms_service.send 被调用了一次,且参数为 (user_id="123")
sms_service.send.assert_called_once_with(user_id="123")
如果 mark_as_paid 里面没有调用 sms_service.send,或者传的参数不对,这个断言就会抛出异常,测试失败。这就是「验证行为」的威力。
关键点: Mock 的重点是「如何被调用」,而不是「返回什么」。即使 Mock 的返回值被忽略,你照样可以写断言。
提供数据:Stub 的职责
Stub 就简单多了:它不关心你怎么调,只保证「你问数据我给你」。在 Python 里,最朴素的 Stub 就是一个普通对象:
class UserStub:
def get_name(self, user_id):
if user_id == 1:
return "张三"
elif user_id == 2:
return "李四"
return None
def test_greeting():
stub = UserStub()
greeting = generate_greeting(stub, 1)
assert greeting == "你好,张三"
# 这里绝不会去检查 stub.get_name 是否被调用了
在我的项目里,经常用一个 Stub 替代 Redis 缓存,直接返回预设的热点数据,测试跑起来飞快,还不用启动 Redis 服务。注意:Stub 本身不会记录调用情况,所以你不能断言它被调用了多少次 —— 想断言?那就得用 Mock 了。
小技巧: Python 的 unittest.mock.Mock 其实同时具备 Mock 和 Stub 的功能。但为了职责清晰,我建议如果你只需要 Stub,就用 MagicMock 并且不写任何断言;或者干脆自建一个普通类,避免滥用 Mock 的「记录能力」导致测试难以阅读。
常见误区与避坑指南
光讲理论不够,我来分享几个我踩过的坑:
- 误区:把所有替身都叫 Mock。 曾经在团队里,大家把所有测试替身统称为「mock」,导致 code review 时有人说「你这个 mock 没有做断言」,但实际上它只是个 Stub……沟通成本直线上升。现在我都会明确区分术语。
- 误区:用 Mock 代替 Stub 做数据提供。 比如直接用
mock_db.query.return_value = [...],然后后面再加一堆assert_called_with,把测试搞得又长又脆。如果只是需要数据,请用 Stub。 - 我曾经 在重构一个支付模块时,把 Stub 改成 Mock 去验证内部的缓存命中逻辑,结果几十个测试因为调用顺序变了而全红。后来发现:我根本不需要验证内部如何调用缓存,只要数据对了就行。改成 Stub 后测试直接稳定。
注意: 不要用 Mock 去验证外部系统的实现细节。Mock 验证的是「你和我之间的协议(接口调用)」,不是「你内部怎么处理」。比如测试订单服务时,你验证它调用了短信服务,但不要验证短信服务内部是否又去调了什么日志接口 —— 那是短信服务的测试。
一张图看懂核心区别
下面这张图总结了 Mock 和 Stub 在思维模型上的根本差异:一个面向交互记录,一个面向数据替代。
画完图再回头看,你会发现 Mock 和 Stub 就像一枚硬币的两面:Mock 面向「交互协议」,Stub 面向「数据契约」。选哪个取决于你要验证什么 —— 如果你关心这个动作是否发生,用 Mock;如果你只关心数据对不对,用 Stub。简单吗?用对了才能写出干净、可维护的测试。