2.3 测试替身实验室:Mock Stub Spy Fake


本节摘要:把外部依赖"关进门"之后,用什么假对象去顶它们,决定测试离真相有多远。替身分四族——Stub 顶返回值、Mock 验行为、Spy 记录调用、Fake 提供简化实现。本节给一张选择流程和四段可运行代码,终结"替身混着用"的糊涂账。

一个名字混乱的来源

Mock、Stub、Spy、Fake 这些词在团队里几乎被用成一锅粥,人人叫"打 Mock",实际干的事却大不相同。之所以乱,是因为它们都长得像:一个帮你摆脱真实依赖的假对象。区别不在长相,在你拿它干哪件事。搞不清这点,最常见的后果是拿 Stub 当 Mock 用,断言一堆根本没人调用的"假交互",让测试越写越紧、越死。

四族替身,各管一段

替身 干什么 你期待的回报 典型场景
Stub 固定返回预设值 一份数据 顶替读接口,省得连库
Mock 验证方法被正确调用过 一条交互断言 确认扣款接口被调到
Spy 记录真实调用,事后查 一份调用日志 包装真对象观察其行为
Fake 提供简化但真的实现 一块能实际工作的替代 用内存表代替真数据库

一句话记住家族关系:Stub 给"数据",Mock 断"行为",Spy 记"过程",Fake 是"缩小头的真身"。Mock 和 Spy 最常被弄混——Mock 是你要验证它被人调了,Spy 是你要看它到底被怎么调的而不打算造假。

下面把四族画进一张选择流程,方便你跺几脚就能落定。

02-03-fig01

四段可运行代码,一次看透

下面用 Python 的 unittest.mock 把四族各跑一遍,你盯着关键差异看。

Stub——固定返回,不关心调用:

from unittest.mock import Mock rate = Mock(return_value=0.1) # Stub 风格:永远回 0.1 assert rate("gold") == 0.1

Mock——确认有人调用过:

billing = Mock() charge(user_id=7, amount=9.99, billing=billing) billing.charge.assert_called_once_with(user_id=7, amount=9.99) # 验证行为

Spy——包真实对象,看它被怎么用:

real = RealLogger() spy = Mock(wraps=real) # Spy 风格:真实执行但留记录 spy.log("hi") spy.log.assert_called_once_with("hi")

Fake——小型真实实现,替代数据库:

class FakeDB: def __init__(self): self.rows = {} def put(self, k, v): self.rows[k] = v def get(self, k): return self.rows.get(k)

Fake 最值得一提的是它不"造假行为",它是一块能真工作的迷你实现,所以它往往最能反映真实集成行为——只要你保证它的口径和生产一致。

别把替身当洪水,也别当万能药

替身有两个极端。一端是过度替身:连第三方 SDK 的整棵对象树都想 Mock,替身比真对象还难维护,测得其实是"测试自己编的假世界"。另一端是从不给替身:单元测试里硬连数据库,等于放弃单元层。正确的姿势是按 2.2 节的账算——快、稳、贵该换的就换,能真用的就用 Fake,想在行为层立契约的就用 Mock 或 Spy。替身的价值不在"假",而在让你能把被测逻辑孤立出来,看清我自己而非外部世界的脸色。

怎么给替身补充"真实感":真实替身与契约

一说替身,人容易想到"全是假的,测不到真相"。这个担忧有解:一是多倾斜向 Fake——它是一块能真工作的迷你实现,比纯粹的 Mock 更接近真身;另一方面是给替身配一层"真实体积"——比如替身读的格式、字段和真实实现保持一致,让替身的行为口径尽量和生产对齐。更进一步,把"我以为外部长什么样"写成契约文件(对应第 3 章的契约测试),让替身永远和真实约定的接口同步,就不会出现"替身记得一套、外部早已改了"的错位。

四种替身的"确定 vs 验证"气质

给四族排个气质轴,能帮你一眼落定。Stub 是"确定性"的——我要它回什么它回什么,我关心出参;Mock 是"验证性"的——我要它证明某方法被调对了几次、带什么参数;Spy 介于两者——它让真实现执行但你事后查看记录;Fake 是"实现性"的——它是真身缩小版,有自己的状态和行为。大多数混淆源于"我到底是要确定出参,还是要验证行为"没想清。想清这层,再翻对应小节的代码,落定就快了。

一个容易栽的坑:在替身上"断言恒真"

用替身时最经典的错误之一,是在替身上做"恒真断言"。比如 Mock 了一个永远 return True 的护栏,再断言"护栏返回 True 时放行",这等于自己和自己对答案,测不出任何真实规则。要避开它,就把 Mock 的返回值设计成"多种真实场景"——True 放行测一条、False 拦截测一条、抛异常测一条,让替身成为可枚举的开关,而不是恒真的死水。这样替身才是在帮你测被测逻辑的分支,而不只是陪衬。

替身选择的一页决策

我的目的 首选 理由
只要一份固定返回数据 Stub 不关心是否被调,只关心出参
要证明某方法被调用过 Mock 验证交互与参数
想看真实对象怎么被执行 Spy 真执行 + 事后查记录
需要一个能真用的小型替代 Fake 最接近真实行为
验证替身口径与外部一致 配契约测试 让替身永远不"落伍"

本节要点回顾

  • 四族定位:Stub 数据、Mock 行为、Spy 过程、Fake 真身缩小。
  • 选法:按"我来测什么目的"选,不是按名字。
  • Mock 与 Spy 之别:前者要验证被调,后者看怎么被调而不造假。
  • Fake 最接近真相:它是能真工作的迷你实现,口径对齐很重要。
  • 别两个极端:过度替身和零替身都是坑,按账算着换。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U