本节摘要:要替身一个外部对象,该用框架自带的替身能力,还是上专门的 Mock 库?本节给出"够用原则"的选型判断——大多数场景原生替身够用,只有高频、复杂、或断言比重极大时才值得上重型库。
上一节讲框架时提过"隔离与替身是内建还是外接"。这一节专门把它掰开:每个语言体系里,要么测试框架自带一套替身(Mockito、pytest 自带的 mock、Jest 的 jest.fn),要么有独立的专业化替身库(如 Java 的 Mockito、Python 的 mocket、JS 的 Sinon.js)。选型不是"非黑即白上专用库",而是先问需不需要。
先摆一张对照表,把两类"药"的分量称清楚:
| 维度 | 原生替身 | 专用 Mock 库 |
|---|---|---|
| 上手成本 | 低,随框架即用 | 中,多一个依赖与语法 |
| 覆盖能力 | 够日常 Stub 与基本行为 | 更强,管道/时序/调度更细 |
| 断言体验 | 常需手拼 | 通常更顺滑、可读 |
| 适用场景 | 80% 常规隔离 | 高频复杂替身才值回票价 |
| 维护负担 | 少 | 多一样要升级的东西 |
要点是:绝大多数场景,原生的就够用。很多团队一上来就把重型库焊进每个测试,往往只是习惯了"标配",而不是真的需要。够用原则省下的,是依赖体积、学习曲线和升级风险。
分别用某语言的"原生"与"专用"写法都覆盖"固定返回 + 验证调用"两件事,你能直观体会够了没有。
原生(示例形态):
from unittest.mock import Mock bill = Mock(return_value="ok") charge(user_id=1, amount=5, billing=bill) bill.charge.assert_called_once_with(1, 5) # 够用,两行
专用库(示例形态):
// 专用库示例:更顺滑的交互断言 when(billing.charge(1, 5)).thenReturn("ok"); verify(billing).charge(1, 5); // 更接近口语,但多依赖一层
读起来专用库的断言确实更像人话,但如果你只做"翻一个人为的 Stub、断一次调用",原生的那两行已经把钱付清了。判断标准回到 6.1 的"四件大事"——断言体验不顺,才值得为它上库。
争分夺秒的三种情况:第一,替身对象很复杂,需要模拟内部状态流转或多次调用的时序;第二,断言比重极大,团队被"手拼断言"折磨,专用库的顺滑能显著提质;第三,你要做细粒度的调用记录或并发行为模拟,原生能力不够用。除此之外,不妨先拿原生替身把测试写完,真被卡住再引入专用库——零成本试错。
| 信号 | 该不该上专用库 |
|---|---|
| 只是固定返回一个值 | 不必 |
| 要断言"被调了几次、参数对不对" | 原生够 |
| 替身内部有状态流转、多次时序 | 上专用 |
| 全仓重依赖断言风格、读起来疼 | 上专用 |
替身库无论多好用,都要记住它是"单元层"的工具,别忘了第 3 章的底线——一旦那段逻辑真正牵扯数据库、网络或真实进程,该验证的就不再是替身能给的,得换上真依赖或契约测试。替身库帮你在单元层把逻辑隔离干净,但绝不替你完成集成层的真验证。这条边界是选型时最容易忘的一课。
选型会上替身库,暗含一种诱惑:因为替身好写,就把所有东西都替身掉,包括那些本就该拿真依赖验的接缝——这就在滑向第 2.5 节的"依赖网络/外部"反模式。所以引入专用库之前,先给自己设一道尺:替身的密度不该无节制增长。若一份测试里替身数量开始逼近被测代码本身,十有八九是这段逻辑已经越界(要么改成了可注入的纯核,要么该把它挪到集成层用真依赖验),而不是替身库选得不够好。
防止替身膨胀有个好用但常被忘的纪律:替身只替"被测对象之外、且非你要验证的那一环"。你要验证订单规则,就把会员等级、存储这两环替掉,但绝不要替掉订单规则本身——替身的作用是隔离边界,不是把被测对象拆成替身的拼图。把这条写进评审口径,能挡掉一大半由替身引发的"测了又好像没测"。
选型不必"从一而终",原生与专用完全可以混用,但这不等于无纪律地到处乱引。比较克制做法是:默认原生替身起步,把某一类"反复被卡"的高频替身单独抽出,只用专用库补那一类,别让它蔓延到整个代码库。这样既拿到了专用库在痛点上的顺滑,又不会让依赖体积和升级负担滚雪球。
真要这么分区管理,注意两点:一是给"什么时候用原生、什么时候用专用"写一句仓库约定,避免同一块逻辑里两种替身写法混着来、读起来精神分裂;二是专用替身越收敛越好,尽量集中在一个"替身工厂"里维护,别让每个测试各自引一套重型库的写法。替身本身是单元层的配角,别让工具反客为主。
无论选原生还是专用,替身能派上多大用场,取决于它有没有三个"记性"。一是记得住"返回值"——某个调用该回什么结果,别让你每次都要造一个会出错的神奇壳。二是记得住"被调过没有/被调了几回"——这是验证"这条路径真的走到、且只走该走的次数"的关键。三是记得住"调用参数"——断言时能回看到底带着什么入参。三样记性齐,一个替身才算活得像个能独立工作的替身,否则它只是个占位的哑桩,测不了交互、也挡不住时序类缺陷。
单从这个角度,原生 mock(如 pytest 的 unittest.mock、Jest 的 jest.fn)往往自带"返回 + 记录"两种记性,够日常大多数用例;只有当你还需要"内部状态流转、多次调用时序、并发记录"这些更深度的记性时,才真正轮到专用库登场。判断该不该升级替身武器,就看你缺的是不是这份"深度记性"。