3.3 mock 与测试替身:边界画在哪


3.3 mock 与测试替身:边界画在哪

本节摘要:测试替身解决的是"被测代码依赖外部世界"的问题:支付要真钱、库存要真仓库,测试不可能陪它玩真的。本节梳理替身家族五类角色的分工,演示 pytest 环境下的标准用法,并立下最重要的一条边界——只替自己拥有协议的东西。读完你应能为任意依赖选对替身,并识别替身过度的症状。

替身这门手艺的行话比想象中老:电影里的特技替身只负责危险动作,正脸还是演员自己演——测试替身也是一样,被测逻辑永远是真的,被替掉的只是协作方。分清"谁在被测、谁在客串",是本节全部内容的浓缩。

替身家族:五类角色

术语来自 Gerard Meszaros 的整理,pytest 圈常混用 "mock" 泛指一切,但五类角色的分工值得分清:

角色 职责 典型用法
dummy 只为填参数签名,从不被使用 传给不看它的参数
stub 喂固定答案,无记忆 查询类接口返回预置值
spy stub 加记忆,记录调用轨迹 事后断言"它被调过、参数为何"
mock 预设期望,调用即校验 交互式协议:必须调、只许调一次
fake 能跑的简化实现 内存仓库、假支付网关

spy 与 mock 的区别常被忽略:spy 是"事后查监控录像",mock 是"事前下逮捕令"。行为风格的测试偏爱 spy(先跑再说),Mockist 风格的 TDD 偏爱 mock(先把期望写出来当设计)。两条路线都能写出好测试,但混着用最容易写出既啰嗦又脆弱的东西。

pytest 现场用法

Python 生态的标准装备是标准库的 unittest.mock,配 pytest-mock 插件的 mocker 夹具用起来更顺手。spy 与 mock 的选择在 pytest 里其实很轻:Mock() 就是 spy 的底座(事后读 call_args_list),加一行 configure_mock 或直接断言就升格为 mock 的用法——同一件工具,两种姿势,选哪种取决于你想"事后查录像"还是"事前下军令"。看灯塔小组怎么替掉支付网关:

def test_successful_charge_returns_confirmation(mocker): gateway = mocker.Mock() # 替身登场:不碰真钱 gateway.charge.return_value = {"status": "ok", "txn_id": "T-01"} checkout = Checkout(gateway=gateway) receipt = checkout.pay(cart_total=Decimal("37.00")) assert receipt.txn_id == "T-01" gateway.charge.assert_called_once_with(amount=Decimal("37.00")) def test_gateway_timeout_triggers_retry(mocker): gateway = mocker.Mock( side_effect=[TimeoutError, {"status": "ok", "txn_id": "T-02"}] ) # 首次调用抛超时,第二次成功 checkout = Checkout(gateway=gateway, retries=1) assert checkout.pay(cart_total=Decimal("37.00")).txn_id == "T-02" assert gateway.charge.call_count == 2

两个惯用法值得记牢:return_value 批发固定答案(stub 用法),side_effect 接异常列表或函数(模拟故障序列)。第一条测试顺带演示了 mock 的设计功能——assert_called_once_with 锁住的是 Checkout 与网关之间的协议:金额怎么传、传几次。先写这条测试,Checkout 的接口就被期望逼出来了,这正是第 4 章"替身即设计"的伏笔。

图:测试替身光谱与边界图

图:测试替身光谱与边界图

边界线:只替你拥有协议的东西

为什么第三方 SDK 不能直接替?因为替身必须模仿真实协作方的行为,而真实行为只有协议文档和你踩过的坑知道。替身里写 return_value={"status": "ok"},万一真实网关成功时还带 fee 字段呢?测试绿了,线上红了。先包适配层再替的妙处在于:适配层薄到可以用少量集成测试验证"它确实翻译对了",单元测试则放心地替适配层——协议从此姓你的姓。

⚠️ 替身过度三症状:其一,改个内部实现红一片测试(替身把实现细节背进了期望);其二,mock 链式打点 mocker.Mock().a().b().c()(隔着两层在审问陌生人);其三,重构后测试全绿线上出事(替身模仿错了行为,测试在保护一个幻觉)。三条症状的处方同源:把替身往边界内侧退,让协议回到你手里。

💡 拿不准用 mock 还是 fake 时,问隔离需求:关心"有没有调、调了几次"用 mock;关心"整条流程能不能走通"用 fake——内存仓库就是最好的 fake,它让订单全流程在毫秒内跑完,还不碰任何真数据。

fake 还有一个被低估的长期红利:好的内存实现是团队资产。换个项目,购物车仓库、优惠券仓库的内存版几乎是通用件;攒上一年,新项目起跑时你有一整箱趁手的 fake——起步速度本身就是竞争力。这也解释了为什么老牌团队宁可自己写内存仓库也不堆 mock:前者越攒越富,后者只随用例越堆越多。

替身还欠一个诚交待:替身模仿的行为,谁来保证像真的?答案是 5.1 的集成层——每个替身对应的适配层,在集成层留几条真环境用例(容器里起真数据库、测试环境打真网关的沙箱),替身写错的行为在那里被拆穿。单元层靠替身求快,集成层替替身保真,两层配对,隔离与真相各归其位。

划下界线

  • 五类角色按隔离需求选,dummy 到 fake 由轻到重。
  • return_value 喂答案,side_effect 演故障,assert_called_once_with 锁协议。
  • 只替边界内侧:第三方先包适配层,替自己的适配器。
  • 替身过度三症状对应同一处方:替身往内退,协议收回来。
  • 骨架三件套到此集齐。下一章换个视角看它们的副产品:测试先行时,设计为什么自己长了出来。

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