Mock 不是万能替身:Dummy/Stub/Spy/Mock/Fake 五种 Test Double 的边界与踩坑

简介: 本文重讲Gerard Meszaros提出的五类测试替身(Dummy/Stub/Spy/Mock/Fake),指出混淆使用导致测试“脆红”或“假绿”。核心原则:**依断言对象选替身**——断数据用Stub,断交互用Mock/Spy,断真实行为用Fake,凑参数用Dummy。AI时代下,该分类更显关键:Stub固模型、Spy录Agent轨迹、Fake造可复现假LLM。

一段很常见的 pytest 代码:测试一个下单服务,作者写下 mock_db = MagicMock(),然后既让它 return_value 返回一条订单,又在结尾 mock_db.save.assert_called_once() 断言它被调了一次。测试过了。可过两周,有人给下单流程加了一步「保存前先校验库存」,多调了一次 db,这条 assert_called_once 就莫名其妙红了——而红的原因和「下单对不对」毫无关系。

同一份代码里,还藏着另一个更隐蔽的问题:那个 MagicMock 到底是在提供数据,还是在验证交互?作者自己大概也说不清,反正「Mock 嘛,万能」。

这就是把所有测试替身都笼统叫「Mock」的代价:测试要么脆得一碰就红,要么假绿——看着断言了一堆,其实什么都没真正验证到。Gerard Meszaros 在《xUnit Test Patterns》里早就把测试替身分成了五种:Dummy、Stub、Spy、Mock、Fake。它们各有明确的用途和断言对象,混用就是 bug 的温床。今天这篇常青翻新,把这五分法按 AI 时代的语境重讲一遍。

一、先把五个名字分清:断言对象决定用哪种替身
先给结论:选哪种替身,不取决于「被替换的东西是什么」,而取决于「你这条测试到底要断言什么」。

Dummy(哑对象):只是为了填满参数列表,测试里根本不用它。断言对象——无。
Stub(桩):提供预设的返回值,把「被测对象依赖的数据」喂成固定的。断言对象——被测对象拿到这些数据后的状态/返回值。
Spy(间谍):记录自己被怎么调用了(调了几次、什么参数),但不改变行为。断言对象——调用发生过没有。
Mock(模拟):预设「期望的交互」,测试结束时验证这些交互是否如约发生。断言对象——交互本身。
Fake(伪实现):一个真的能跑、但轻量的实现,比如内存数据库。断言对象——被测对象在真实行为下的状态。
一句话记法:Stub 管「输入数据」,Mock/Spy 管「交互行为」,Fake 管「真实但轻量的实现」,Dummy 管「凑数」。 开头那段代码的病根,就是把「提供订单数据」(该用 Stub)和「验证 save 被调一次」(Mock 交互断言)揉进了同一个 MagicMock,结果一条本该只验数据的测试,被交互断言拖累得极脆。

二、Stub:只提供数据,别顺手断言交互
Stub 的职责单一——被测对象问它要什么,它就返回预设的什么,测试只断言被测对象自己的输出。

from unittest.mock import MagicMock

def test_order_total_uses_discount():

# Stub:只负责返回固定的折扣率,不关心它被调了几次
pricing = MagicMock()
pricing.get_discount.return_value = 0.1     # 提供数据

from service import calc_total
total = calc_total(pricing, base=100)        # 100 * (1 - 0.1)

assert total == 90                           # 断言被测对象的返回值,不断言 pricing 的调用

为什么这么写、踩过什么坑。 这里刻意没有写 pricing.get_discount.assert_called_once()。Stub 的价值就在于稳定:只要它返回 0.1,calc_total 该算出 90 就算出 90,至于内部调了一次还是两次折扣查询,那是实现细节,不该被这条测试锁死。最常见的坑就是给 Stub 顺手加交互断言——一旦实现重构(比如加了缓存、少调一次),测试就假红。记住:你断言的是返回值,就别再断言调用次数。

三、Spy:记录调用序列,AI 场景里它对应 Trajectory
Spy 记录「被怎么调用了」,但把行为留给真实实现或默认值。它在 AI 测试里格外重要——当被测对象是 Agent,你想验证它「按什么顺序调了哪些工具」,用的就是 Spy 的思路。

from unittest.mock import MagicMock

def test_agent_calls_tools_in_order():
tools = MagicMock()
tools.query_order.return_value = {"status": "paid"}
tools.do_refund.return_value = {"ok": True}

from agent import run_refund_agent
run_refund_agent(tools, order_id="A1001")

# Spy 式断言:记录并校验调用序列(呼应 Trajectory 断言)
called = [c[0] for c in tools.method_calls]
assert called == ["query_order", "do_refund"], f"工具调用顺序不对:{called}"

为什么这么写、踩过什么坑。 Spy 断言的是「调用发生过、且顺序对」,这正是本日旗舰那篇讲的 Trajectory 断言的最小雏形——把 Agent 的工具调用序列记录下来做校验。坑在于用 Spy 去顶替本该用 Fake 的场景:比如你想测「订单存进库再查出来对不对」,用 Spy 记录 save 被调了没有任何意义,因为你根本没验证数据真的存进去了——这时候要的是 Fake。Spy 只回答「调没调、怎么调」,不回答「结果对不对」。

四、Mock:断言交互,且只在交互本身是契约时才用
Mock 预设期望的交互并在结尾验证。它适合的场景很窄:当「有没有发出这个交互」本身就是被测契约时——比如「必须发一条审计日志」「必须调用一次风控」。

from unittest.mock import MagicMock

def test_payment_triggers_audit_log():
auditor = MagicMock() # Mock:交互本身就是契约
from service import process_payment
process_payment(auditor, amount=100)

# 断言「审计日志被以正确参数记了一次」——这就是本测试要验的契约
auditor.log.assert_called_once_with(event="payment", amount=100)

为什么这么写、踩过什么坑。 这里用 Mock 是对的,因为「支付必须留审计痕迹」是一条真实契约,交互本身就是被测目标。但 Mock 是最容易被滥用的替身:很多人把每个依赖都换成 Mock、给每条测试堆一摞 assert_called_with,结果测试全在验证「内部怎么调的」,没有一条验证「对外产出对不对」。这种测试改一次实现就红一片,是脆弱测试的头号来源。判断标准很简单:如果这个交互不是对外契约、只是实现细节,就别用 Mock 断言它。

五、Fake:真能跑的轻量实现,AI 场景里替掉不确定的模型
Fake 是一个真实但轻量的实现,最经典的就是内存数据库。它不记录交互、不预设返回,而是真跑一遍逻辑,让被测对象在接近真实的行为下被验证。

class InMemoryOrderRepo:
"""Fake:内存版订单仓储,真存真取,替代真实数据库"""
def init(self):
self._data = {}
def save(self, order):
self._data[order["id"]] = order
def get(self, order_id):
return self._data.get(order_id)

def test_order_roundtrip():
repo = InMemoryOrderRepo() # Fake
from service import place_order
place_order(repo, {"id": "A1", "sku": "X"})
assert repo.get("A1")["sku"] == "X" # 断言真实存取后的状态
为什么这么写、踩过什么坑。 Fake 的价值在于「真的存进去、真的查出来」,验证的是端到端的状态正确,而不是某个方法被调了。在 AI 场景里,Fake 有一个特别贴切的用法:把不确定的大模型调用替成一个行为确定的假模型。 真实模型每次输出都可能不同,测试没法稳定断言;写一个 Fake LLM——按输入规则返回固定响应——就能让 Agent 测试变得可复现。这比用 Stub 硬塞一个 return_value 更强,因为 Fake 能模拟「不同输入给不同输出」的真实决策逻辑,让被测 Agent 的多步流程真正跑起来。

五点五、一个 code review 就能用的识别口诀
五分法讲完,落到日常最实用的其实是一条 review heuristic:看一条测试的断言写在谁身上。 断言写在被测对象的返回值/状态上,那它用的多半是 Stub 或 Fake,健康;断言写在一堆替身的 assert_called_with 上、被测对象自己的产出反而没验,那这条测试大概率被 Mock 滥用了,是脆弱测试的高危信号。再补两条快速判据:其一,一条测试里最好只有一种「主替身」承担核心职责,如果你发现同一个 MagicMock 既在 return_value 提供数据、又在结尾被断言调用次数,基本就是开头那段代码的病——把它拆成一个 Stub 加一个 Spy,各自职责单一。其二,替身能不能被轻松替换,取决于被测代码有没有把依赖作为参数传进来(依赖注入);如果依赖是在函数内部 new 出来或全局 import 死的,你连替身都塞不进去,只能靠 patch 打补丁,测试自然又脆又难读。所以「替身用不对」往往不只是测试的问题,它会在 review 里反向暴露出被测代码耦合过紧的设计问题——这也是把五分法讲清楚的另一层价值。

六、五种替身一张表:用途、断言对象、典型误用、AI 场景
把五分法汇总成一张对照表,选型时对着查即可:

替身
用途
断言对象
典型误用
AI 场景对应做法
Dummy
凑满参数,不使用
无
给 Dummy 加断言
占位传入不相关的上下文对象
Stub
提供固定返回值
被测对象的返回值/状态
顺手断言调用次数
把模型调用 Stub 成固定响应
Spy
记录调用序列
交互是否发生、顺序
用 Spy 顶替该用 Fake 的存取验证
记录 Agent 工具调用做 Trajectory 断言
Mock
验证期望交互
交互本身(参数、次数)
把每个依赖都 Mock、堆一摞脆弱断言
断言「必须触发风控/审计」这类契约
Fake
真实但轻量的实现
端到端状态正确性
用 Stub 塞死返回值代替真跑逻辑
写一个按规则决策的假模型/内存向量库
这张表的核心不是背下五个名字,而是那条选型主线:先问「我这条测试要断言什么」,再倒推该用哪种替身。 断言数据用 Stub,断言交互用 Mock/Spy,断言真实行为用 Fake,凑数用 Dummy。把这条主线立起来,开头那种「一个 MagicMock 既提供数据又断言交互」的混用就不会再出现。

写在最后
「Mock 万能」是个流传很广的误解,它让无数测试要么脆得一碰就红、要么假绿得什么都没验。五种 Test Double 的分界从来清晰:断言对象是数据、是交互、还是真实行为,决定了你该伸手拿哪一种。

到了 AI 时代,这套老分类不但没过时,反而更重要——被测对象从确定代码变成了不确定的模型和 Agent,你更需要用 Stub 把模型固定住、用 Spy 把工具调用序列记下来、用 Fake 造一个行为可复现的假模型。替身多替了一层,但选型的那条主线一点没变。

别问「这个依赖该不该 Mock」,先问「我这条测试到底要断言什么」——答案会自己告诉你该用哪种替身。

相关文章
|
2月前
|
人工智能 缓存 Java
2026年美团职级薪资:测试开发L5—L10能拿多少?附AI测试社招面试题
本文解析2026年美团测试开发社招趋势:L5-L10年包44万–419万,股权占比随职级显著提升;能力要求已从传统自动化进阶为“基本功+AI测试”,涵盖Agent评测、大模型UI测试、AI Coding质量保障等实战方向,并附薪资、职级、面试与技能树全景指南。
|
2月前
|
JSON 人工智能 测试技术
Agent Skills、MCP、Function Calling 到底啥区别?一文讲透
本文厘清AI Agent三大核心概念:Function Calling(模型调用工具的“嘴”,负责结构化指令)、MCP(标准化连接协议, akin “插座”,解耦模型与工具)、Agent Skills(封装业务逻辑的“经验包”,含流程、异常处理与复用能力)。三者呈层级协作关系,非替代关系,共同构建可落地的自动化测试体系。
|
2月前
|
缓存 JSON 安全
最新版通义千问Qwen3.8-Max保姆级手册:多模态能力、长周期Agent任务与代码调用全流程
Qwen3.8-Max作为旗舰级多模态MoE大模型,依托2.4万亿总参数、动态激活950亿专家参数、百万Token超长上下文、混合注意力架构,在复杂专业推理、长周期Agent任务、大型代码工程、多模态深度理解等场景实现能力突破。模型内置联网检索、代码沙盒、网页提取等全套工具能力,兼容主流API协议,支持上下文缓存、结构化输出等企业级能力,开发者既可以快速调用云端API,也可以私有化本地部署。
347 0
|
2月前
|
存储 人工智能 安全
千问办公收费吗?个人版和企业版有什么区别?支持免费使用吗?
阿里云千问办公(QwenWork)提供个人版(含免费版)与企业版:免费版赠2000积分+每日登录送100分;个人标准/高级版折后78/158元/月;企业版198/298元/席/月,支持VPC部署与统一管控。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
306 0
|
2月前
|
前端开发 JavaScript 定位技术
支持省市区县乡镇村GeoJSON 在线预览编辑下载器开源:一个纯前端实现的拆解
这是一个纯前端、零依赖的GeoJSON轻量预览编辑器,支持本地打开、WGS84坐标渲染、要素查看/编辑,并可导出为GeoJSON/KML/SVG/CSV/PNG。数据不上传,双击index.html即用,开箱即用,适合快速验图、格式转换与二次集成。
|
2月前
|
人工智能 缓存 监控
QoderWake联合云电脑:让数字员工随时待命工作
QoderWake联合无影云电脑上线,为数字员工Waker提供持久化云端运行环境:关机不中断任务、远程实时查看与接管操作、环境状态自动保留。支持浏览器/终端/桌面全场景自动化,适用于竞品监控、夜间构建、定时报表等长时任务。
360 0
|
2月前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
2550 2
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
2月前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)
|
2月前
|
人工智能 搜索推荐 架构师
2026测试人自救指南:把AI变成你的第二个测试搭子
这不是让AI替你干活,而是为你配备一位24小时在线、懂业务、会协作的“测试搭子”。它不替代判断力,而是帮你高效生成用例、分析失败、补全场景——关键在于换种方式“说话”:给足背景、约束、范围和格式。你负责深度决策,AI负责广度执行。
|
2月前
|
供应链
拆解1688新品三档分级:从等级准入到供应链准入的换轨信号
1688消费品新品成长体系于10月8日升级:取消统一认定,按稀缺性分S/A/B三档;商家准入由等级门槛转向供应链资质(如二星及以上);强化反套利规则,新增真实性校验(如买家数+五维指数)。本质是从“奖励达标”转向“奖励稀缺”,凸显B2B供给侧治理新逻辑。