假设你是《订单快递》这部戏的导演:主角要演一场“给顾客发发货短信”的戏,可你不能真的让演员去邮局跑一趟——太慢、太依赖天气、还可能真的把信寄出去。于是你请来一个替身演员,长得像、台词熟,站在台上配合主角把戏演完。这个替身演员,就是单元测试里的测试替身(Test Double)。 替身也分两种:一种是“道具型替身”,导演喊“报单号”,他就机械地回答“10086“,从头到尾不需要真本事,这叫 Stub(桩);另一种是”记账型替身“,他不仅对台词,还会在小本子上记下”某时某刻导演让我说了哪句话、顺着什么手势递了快递单”,散场后导演翻本子来确认主角有没有按剧本走位,这叫 Mock(模拟对象)。 Stub 和 Mock 看起来都是“找人代替真身”,但一个只负责提供预设答案,一个还要记录并验证交互过程。弄懂这层区别,你写单元测试时就知道:什么时候该找个桩站在那儿,什么时候该安个摄像头盯着对手戏。
对 AI 从业者来说,模型推理、数据管线、外部服务调用都依赖接口协作,用对 Stub 和 Mock 能让你写出稳定不脆的单元测试,保证 AI 工程交付质量。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
概念与定位
片场里有两种替身,你在监视器前一眼就能分清:Stub 是道具型替身,导演问什么台词,他就照本宣科回答预设好的那一句,多一个字都没有;Mock 是记账型替身,他答完台词还会低头在小本子上写两笔,记下“刚才谁给我递了眼神、我回了几句话、顺序如何”。这两种替身,共同构成单元测试里最常用的测试替身阵容。
在单元测试语境里,“被测系统”就是舞台上的主角,外部依赖(数据库、邮件服务、AI 模型接口)是需要被替代的配角。当你只想让配角“站在那里配合演出”时,用 Stub 就够了;当你担心主角“有没有真的按照剧本做动作”时,需要的是 Mock。理解这一层,是进入测试工程的大门钥匙。
给替身安排岗位时,记住 Stub 是“工具人”,Mock 是“见证人”。你不需要让见证人背台词,也不该让工具人记笔记,选对角色,戏才不塌。
- 测试替身这是一个容器概念,代表所有在测试中替代真实对象的程序片段。
- Stub特性是“输入固定、输出预设”,像一台自动售货机——按键必出货,且货永远相同。
- Mock特性是“先记录、后判决”,像带录音笔的演员,既表演也留证据。
核心区别
散场之后,两种导演的工作方式不同。只关心结果的导演,把 Stub 返回的单据拿过来,看一眼金额对不对就收工回家——这叫状态验证;较真的导演则翻开 Mock 的小本子,核对“主角是什么时候递的单据、递给了谁、说了哪句台词”——这叫行为验证。
对应到代码上,状态验证的断言对象是返回值:被测模块调用 Stub 拿到结果,再与预期值比对。行为验证的断言对象是调用过程:Mock 记录下“被调用了哪些方法、传入了哪些参数、调用了多少次”,测试通过查询这些记录来确认行为符合预期。
举个直白的例子:测一个订单服务,Stub 方式断言“返回的订单状态是已发货”,Mock 方式断言“send 方法被调用过一次,参数收件人是 alice@example.com”。前者测商家的结算逻辑,后者测流程的传递动作,各管一段。
- 返回值与预期值状态验证把 Stub 返回的数据当作“证据”,只比对数值或对象状态。
- 记录与预期调用行为验证把 Mock 的调用记录当作“证据”,检查列表中是否包含预期的那次调用。
- 等号与包含号等号表示“结果精确匹配”,包含号表示“该调用必须发生过”,两种符号恰好对应两种验证哲学。
适用场景
现在你坐在导演椅上,手里有两份剧本。第一份剧本里,主角需要知道“今天的快递费价格表”才能算运费——这种事只需要一个人站在那儿报个数字,Stub 出场;第二份剧本里,主角负责“呼叫仓库发货”,你必须确认他真的呼叫了仓库而不是自己把货搬走了——这种交互必须留痕,Mock 上场。
工程上有个简单判断法:如果测试失败只是因为返回值不对,用 Stub;如果测试失败是因为依赖没有被正确调用(顺序、次数、参数不对),用 Mock。读取配置、查数据库、调用计算服务,通常 Stub 足够;发消息、写日志、调第三方 API,通常需要 Mock。
如果你发现测试里塞满了 Mock 才过得去,先别急着写替身,问问设计是否合理。好的单元测试跟好导演一样,能删的戏份绝不硬加。
- 返回结果方向被测代码把外部依赖当“数据源”时,注入 Stub 提供稳定数据即可。
- 调用过程方向被测代码把外部依赖当“动作执行者”时,必须用 Mock 记录动作是否发生。
- 方向箭头箭头表示“考察的重点决定选择”,而不是“某个库或框架强制绑定”。
编写实践
到了实操环节,给替身安排什么装备?Python 里自带 unittest.mock 库,一行 mock.Mock() 就能制造一个多功能替身:用 .return_value 设定 Stub 的预设答案,用 .assert_called_once_with(...) 做 Mock 的行为验证——同一个对象,两种用法,上得厅堂下得厨房。Java 阵营响当当的 Mockito 同理:when(mock.price()).thenReturn(80) 是 Stub 手感,verify(mock).send(email) 是 Mock 手感。
写的时候抓住三条要领:第一,替身只替外部边界,不替内部值对象;第二,Stub 配置写“当碰到什么入参、返回什么结果”,Mock 断言写“期望这个方法被带这些参数调用 N 次”;第三,测试结尾先查行为再查状态,顺序别乱。
在 Python 里,mock.Mock() 对象既能当 Stub 也能当 Mock,这常让新手误以为二者没有区别。其实区别不在工具而在断言动作:你不去查调用记录,它就是个 Stub 的命;你调用 assert_called_*,它就觉醒成 Mock 了。
preset 是 Stub 的“喂饭”动作,verify 是 Mock 的“翻账本”动作。- 替身。预设值这是 Stub 的配置语句,相当于设定自动售货机的出货内容。
- 状态准备预设好返回值后,被测代码只需正常调用,Stub 便返回设定数据。
- verify(替身)这是 Mock 的验收语句,相当于查看替身的小本子。
- 行为验收验证“某方法被调用、参数符合、次数符合”,通过则测试通过。
最佳实践
一个成熟的导演,选替身有几条铁律。第一,替身数量最小化——能用真实组件的地方绝不引入替身,替身只出现在测试与被测代码的真实边界上;第二,命名清楚——stub_price、mock_email_sender 一眼能看出身份,别用 mock1、fake2 糊弄人;第三,先考虑真实依赖——内存数据库、本地测试容器能用就先用,替身是最后一道屏障。
把“测试替身”想象成剧场里的消防通道:修太多反而堵了主路。过度 Mock 会让单元测试变成“自说自话”——测试验证的是 Mock 的行为是否符合 Mock 的配置,而不是真实系统的行为。越少的替身意味着越少的维护负担。
写测试时问自己一句:删掉这个替身,测试会崩吗?如果不会崩,请删。留下必要的最小替身阵容,测试会跑得更快、更稳、更可信。
- 维护成本包括替身配置的修改、Mock 断言随接口变化而调整的工作量。
- 替身数量测试中引入的 Stub / Mock / Fake 数量总和。
- 平方关系替身之间还存在隐性联动,新增一个替身可能逼着调整另一个替身的参数,成本呈非线性上涨。
家族对比
把替身剧场转一圈,你会发现还有几位“编外”成员。Dummy 是背景板演员,只是来凑个人数让构造函数不报错,台词和戏份都不参与;Fake 是“缩略版剧组”,比如用内存版数据库替代真实数据库,它内部有真实业务逻辑但运行得很快;Spy 是“带录音功能的 Stub”,在真身外面包一层记录调用的壳;而 Stub、Mock 两位正是这串能力阶梯上的核心。
- Dummy(哑元):占位用,无逻辑,比如只传空对象不让构造器崩溃。
- Fake(假实现):简化版真逻辑,比如内存数据库、静态字典模拟表。
- Stub(桩):预设返回,管“结果对不对”。
- Spy(间谍):真实对象 + 调用记录,半 Stub 半 Mock。
- Mock(模拟):记录调用 + 断言行为,管“过程对不对”。
选替身时,先问自己“我需要哪个档次的能力”,而不是“哪个听起来高级”。Dummy 和 Fake 是 Stub 与 Mock 的邻居,它们一起撑起了测试隔离的地基。
- Dummy(哑元)只解决“参数不能为空”的问题,不产生任何真实行为。
- Stub(桩)在 Dummy 基础上增加“返回预设值”的能力。
- Spy(间谍)在 Stub 基础上增加“记录真实调用”的能力。
- Mock(模拟)在 Spy 基础上进一步把“断言调用记录”作为内置能力。
常见陷阱
片场最容易翻车的,是导演自己把道具和账本搞混。第一大事故:把 Mock 当 Stub 用——测试只关心返回值,却给 Mock 加了一堆 verify 断言,结果接口内部稍微改个参数名,测试就哗啦啦全红,修都修不完。第二大事故:过度 Mock——每个类都配一个 Mock,测试跑得慢、改起来累。第三大事故:行为耦合——连“临时变量怎么组合”都去断言,把 Mock 当成了代码显微镜。
深挖下去,问题根子在于:把“实现细节”错当“行为契约”去验证。行为验证应该只针对“对外部世界产生影响的调用”(比如发了邮件、写了消息),而不是针对内部计算步骤。Stub 与 Mock 的正确用法,是三分靠配置、七分靠克制。
记住这个画面:替身是舞台边缘的钢管,扶一把保持平衡就好,别让演员全场抱着钢管跳舞。Stub 与 Mock 用得好,是测试的加速器;用乱套,就是测试的定时炸弹。
- 替换难度指后续重构时,修改测试替身所付出的成本。
- 被 Mock 接口数测试中为外部依赖创建的替身数量。
- 真实边界结论替身只应出现在“系统与外部世界”的接触点上,即 I/O 边界附近。
AI 应用场景
场景一:AI 模型调用链路的单元测试
阶段:模型服务集成与测试
解决的问题:在 NLP 服务里,业务代码会调用大模型 API 完成意图识别。真实调用慢、贵、且结果随机,无法在 CI 中做稳定断言。传统做法是直接连真实 API,跑一次测试花费数秒并产生费用;现在用 Mock 替代模型客户端,验证“prompt 是否拼装正确、请求超时参数是否设置到位”。
具体案例:某客服机器人项目,测试用 mock.Mock() 替换 LLMClient,设置 .chat.return_value = "退款",随后断言 chat 被调用一次且传入的 system_prompt 包含用户画像信息。这样既验证了取回意图的分类逻辑(Stub 职责),又验证了 prompt 确实被发出(Mock 职责)。
场景二:推荐系统特征管线的隔离测试
阶段:特征工程与数据管线测试
解决的问题:推荐服务依赖特征存储 SDK 拉取用户画像,一旦特征服务抖动,整条管线测试全挂。用 Fake 内存特征表替代真实 SDK 可快速跑通逻辑,但若想验证业务层“确实请求了指定维度的特征”,还需要 Mock 来记录请求参数。
具体案例:在特征调用处注入 Mock 的 FeatureClient,断言 get_feature("user_tag", user_id=1024) 被调用且包含 recency 维度,同时用 return_value 预设特征字典,让下游排序逻辑稳定运行。最终测试总耗时从 12 秒降到 0.3 秒。
Python 演示
# demo_mock_stub.py# 演示 Mock 与 Stub 在 Python unittest.mock 中的两种用法from unittest import mock class OrderNotifier: def __init__(self, email_sender): self.email_sender = email_sender def notify(self, order_id, user_email): subject = "订单 %s 已发货" % order_id self.email_sender.send(user_email, subject) return "sent:%s" % order_id class DiscountService: def __init__(self, policy): self.policy = policy def final_price(self, order_id, base_price): rate = self.policy.get_discount(order_id) return base_price * rate def test_notify_uses_mock_to_verify_behavior(): # 创建 Mock 替身:记录 send 调用过程 mock_sender = mock.Mock() notifier = OrderNotifier(mock_sender) result = notifier.notify(10086, "alice@example.com") # 行为验证:断言 send 被调用且参数正确 mock_sender.send.assert_called_once_with( "alice@example.com", "订单 10086 已发货" ) assert result == "sent:10086" def test_final_price_uses_stub_to_provide_value(): # 创建 Stub 替身:只提供固定折扣率,不关心是否被调用 stub_policy = mock.Mock() stub_policy.get_discount.return_value = 0.8 service = DiscountService(stub_policy) # 状态验证:只断言最终价格 assert service.final_price(7, 100) == 80 if __name__ == "__main__": test_notify_uses_mock_to_verify_behavior() test_final_price_uses_stub_to_provide_value() print("两个测试均通过:Mock 验证了行为,Stub 提供了数据")
mock_sender = mock.Mock()创建万能替身,默认能接收任何方法调用并记录过程。mock_sender.send.assert_called_once_with(...)这是 Mock 的“翻账本”动作,断言 send 恰好被调用一次且参数精确。stub_policy = mock.Mock()同一个类也可以当 Stub 用,关键在于你后续如何配置和断言。stub_policy.get_discount.return_value = 0.8Stub 的“喂饭”动作,设定返回值为固定折扣率 0.8。assert service.final_price(7, 100) == 80状态验证:只关心计算结果是否正确,不检查 policy 是否被调用过。
常见误区
误区我在代码里都是用 `mock.Mock()` 写测试,既设返回值又做断言,它既像 Stub 又像 Mock,所以这两个词没区别。
正解工具可以被复用,但概念不能混淆。Stub 的职责是“给定输入返回预设输出”,它不关心自己是否被调用、被调用几次;Mock 的职责是“记录调用过程并允许断言”。同一个 Mock 对象,只设返回值时它扮演 Stub,加上 assert_called_* 断言时它才成为真正的 Mock。就像一把螺丝刀既能拧螺丝也能撬盖子,但撬盖子时它扮演的是撬棍。
为何常见:因为 unittest.mock 和 Mockito 都让一个对象同时具备两种能力,新手只看 API 不看语义,自然以为二者等价。
误区单元测试要隔离一切依赖,所以数据库、内存缓存、配置文件、时间函数全部用 Mock 替换,这才算“干净”的测试。
正解过度 Mock 是测试腐化的头号原因。测试的价值在于验证真实行为,而真实行为的一部分恰恰由依赖的交互定义。能用真实组件(如内存数据库 SQLite、本地测试容器)跑通的就别 Mock;Mock 应该留给“真实组件太慢、太贵、有副作用”的场景(如邮件服务、第三方支付 API)。替身越少,测试越接近生产真相。
为何常见:教材里大量强调“隔离依赖”,初学者把“隔离”理解成“全部替换”,忽视了测试的根本目标是信心而非纯度。
误区Mock 能记录所有调用,那我多写几个 `assert_called_with` 就能把测试做得更严格,防止任何一步逻辑出错。
正解行为验证应该只针对“对外部世界产生可观察影响”的调用,比如发了邮件、写入消息队列、调用了第三方 API。内部计算步骤的中间值、临时变量组合都属于实现细节,验证它们会让测试与实现强耦合——以后你优化内部代码结构时,行为没变但测试全红,这被称为“测试过于耦合实现”。
为何常见:新手刚会写 verify 时容易上瘾,觉得断言越多越安全,忘了测试应该聚焦“契约”而非“脚注”。
误区我写了个内存版数据库类,它实现了真实的增删改查逻辑,这不就是一个功能更强的 Mock 吗?
正解Fake 是有真实业务逻辑的简化实现,它的行为是“真正工作的”,只是运行在内存中;Mock 没有业务逻辑,它的行为完全由你的脚本预设。用 Fake 替换真实数据库测的是“数据访问层在真实查询语义下是否正确”;用 Mock 替换数据库测的是“上层代码是否发出了正确的调用”。二者适用层面完全不同,不能随意互换。
为何常见:Fake 和 Mock 都实现了接口、都不碰真实基础设施,表面相似性掩盖了“有无真实逻辑”的本质差异。
面试题
Q1. Stub 和 Mock 共同的上位概念是什么?
- A.测试框架
- B.测试替身← 正确答案
- C.桩代码
- D.断言工具
答案:B。Stub 和 Mock 都属于“测试替身”家族,它们替代真实依赖以隔离被测单元。测试框架和断言工具是实现手段,桩代码是 Stub 的别名表述,都不够全面。
Q2. Stub 的核心职责是什么?
- A.记录调用次数并报告
- B.验证方法调用顺序
- C.返回预设的固定数据← 正确答案
- D.替代整个测试环境
答案:C。Stub 的核心是“状态供给”,即入参进来、预设值出去。记录调用、验证顺序是 Mock 的职责,替代整体环境不是 Stub 的定位。
Q3. Mock 相比 Stub 多出来的独特能力是?
- A.可以设置返回数值
- B.可以抛出异常
- C.验证行为发生的过程← 正确答案
- D.自动生成单元测试
答案:C。设置返回值、抛异常都是预设行为的能力,Mock 和 Stub 都能做到。Mock 的独特之处在于“行为验证”——断言方法是否被调用、参数与次数是否符合预期。
Q4. 下列哪项属于典型的状态验证?
- A.断言 send 被调用一次
- B.断言返回订单金额等于八十← 正确答案
- C.断言消息发送顺序正确
- D.断言入参包含用户编号
答案:B。状态验证关注“返回结果是否等于预期值”,断言订单金额就是典型例子。其余三项都在检查方法调用过程,属于行为验证范畴。
Q5. 以下哪些场景更适合使用 Mock?
- A.读取用户配置并拼接待发文本
- B.验证邮件发送接口被正确调用← 正确答案
- C.记录消息队列的推送参数← 正确答案
- D.获取数据库单条固定记录
答案:BC。Mock 适合“需要确认交互发生”的场景。验证邮件发送、记录消息推送都属于行为验证,适合 Mock;读取配置、获取固定记录只需数据返回,用 Stub 更轻量。
Q6. 过度使用 Mock 可能带来哪些危害?
- A.测试运行速度显著变慢← 正确答案
- B.测试对实现细节过度耦合← 正确答案
- C.接口重构时大批测试失败← 正确答案
- D.测试执行顺序发生颠倒
答案:ABC。Mock 创建多、断言多,会拖慢测试速度、使测试与实现捆绑,接口一改测试就崩。但测试执行顺序由测试框架管理,不因过度 Mock 而颠倒。
Q7. Dummy 测试替身的主要作用是?
- A.记录所有内部状态变化
- B.验证参数传递顺序
- C.占位以通过参数校验← 正确答案
- D.返回预设的复杂对象
答案:C。Dummy 只做“占位”一件事,让构造函数或参数列表不报空值错误,不参与任何业务逻辑。记录状态、验证顺序、返回复杂对象分别是 Spy、Mock、Stub 的活。
Q8. Fake 与 Mock 的本质区别是什么?
- A.Fake 也有真实业务逻辑← 正确答案
- B.Fake 只能用于集成测试
- C.Mock 必须来自第三方库
- D.Mock 不能预设返回值
答案:A。Fake 是带真实简化逻辑的实现(如内存数据库),Mock 是行为完全由脚本预设的空壳。Mock 既可来自框架也可手写,预设返回值是 Mock 与 Stub 的通用能力。
Q9. 以下哪些属于 Mock 测试的最佳实践?
- A.只替身化真实外部边界← 正确答案
- B.最小化替身引入数量← 正确答案
- C.验证具体参数而非模糊匹配← 正确答案
- D.对所有依赖一律 Mock
答案:ABC。最佳实践强调替身仅用在真实边界(如 I/O 接口)、数量尽可能少、断言参数写具体值。对所有依赖一律 Mock 正是需要避免的过度 Mock 反模式。
Q10. 测试金字塔对替身使用的核心启示是什么?
- A.用尽量真实的组件跑通底层测试← 正确答案
- B.所有层的测试都要 Mock 依赖
- C.Mock 使用越多测试越可靠
- D.集成测试必须完全真实无替身
答案:A。测试金字塔主张底层单元测试应接近真实、用替身隔离外部副作用,越底层越偏向真实组件、Mock 越少,整体测试才稳固。全部 Mock、越多越可靠都是完全跑偏的理解。