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

简介: 本文厘清测试替身的五大分类(Dummy/Stub/Spy/Mock/Fake),指出滥用“万能Mock”导致测试脆弱或假绿的根源:混淆“提供数据”与“验证交互”。强调选型核心原则——**先明确断言对象(数据?交互?真实行为?),再决定替身类型**,并结合AI场景(如Agent轨迹验证、假模型替代)赋予经典模式新生命力。

一段很常见的 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」,先问「我这条测试到底要断言什么」——答案会自己告诉你该用哪种替身。

相关文章
|
9天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
|
9天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。
|
15天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
10天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1912 15
|
8天前
|
IDE 开发工具
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
Qoder国际版上线全新内置大模型Sonus(/ˈsoʊnəs/),全球领先,专精超长任务执行与电脑操作(Computer Use)。配合Qoder桌面端0.2.3版本,可自主完成编程、金融建模、科研及表格制作等复杂工作。现全面支持Qoder全系产品,效率提升3.2倍。
1034 1
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
|
14天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1670 4
|
10天前
|
缓存 人工智能 自然语言处理
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
本文是阿里云百炼平台Qwen3.8-Flash大模型的选型接入指南,作为兼顾性能与响应速度的高性价比多模态模型,它支持百万级上下文窗口、全场景多模态输入与完整智能体能力矩阵,适配编程辅助、智能体协作等核心场景。文中同步梳理了最新下调的阶梯定价、夜间4折等优惠活动,搭配OpenAI兼容流式调用示例,帮助开发者低成本快速落地高并发AI应用。
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
|
16天前
|
缓存 数据可视化 开发工具
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
DeepSeek Harness 的更新分两层:本体更新(npx 自动最新、npm update -g、源码 git pull)与插件更新(插件市场点更新、命令行覆盖安装)。本文按「准备 → 更新本体 → 更新插件 → 更新后检查」四步走,覆盖新手常见疑问。
1831 1
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
|
11天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
829 2
|
9天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
832 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)

热门文章

最新文章