一个补丁落进小型工具库:补上分页辅助函数缺失的测试,修掉一个向上取整的 bug,还在同一个提交里把两条旧断言改写成更宽松的 >= 比较。CI 全绿,diff 读起来干净,评审九十秒就过了。六周后,一次分页回归恰好从那些被放松的断言原本守着的代码路径里溜了过去。
这个场景是合成的,不是某起具体事故,但可以在本地用大约十分钟重建。整套流程跑在一个四文件的夹具仓库上,任何读者都能用文中给出的命令搭出来。目标很机械:检查一个补丁的测试是否真的观察到了补丁的行为,而不是仅仅和它待在一起。
全绿的测试套件到底证明了什么?它只回答一个很窄的问题:已经存在的断言没有失败。它没说这些断言够不够严格,也没说新测试放到未修复的代码上会不会照样通过。评审者通常靠注意力来补这个缺口,而注意力恰恰是周五下午最先耗尽的东西。
有三个廉价信号能补回大部分缺失的信息。第一,新测试在基线提交上会不会失败。第二,把修复拿掉、测试保留,套件会不会失败。第三,这个补丁删掉或放松了多少行断言。
每个补丁都该走一遍的三个状态
每个状态都是一个分离的 Git 工作树,不用来回切分支,也没有 stash 的风险:
- 基线状态——上游最新提交,只加上贡献者的测试改动。新测试必须在这里失败,而且必须是因为预期原因失败。
- 打补丁状态——应用贡献者的完整 diff。套件必须在这里通过。
- 变异状态——打上补丁的树,但把 src/ 回退到基线,测试原样保留。套件必须在这里失败,否则测试根本没有观察到修复。
第三个状态是大多数评审流程会跳过的那一个,也是唯一能识别出“照着修复写、而不是用来约束修复”的测试的状态。
第一步:搭夹具仓库
夹具刻意做得很小:一个模块、一个测试文件、一个 bug。这个 bug 是缺少向上取整,导致任何小于页大小的总数都返回零页。
在基线上,src/paging.py 里的函数只做整除:total // size。测试文件里有三条断言,分别覆盖整除、大总数和零总数。把它们提交为 main,然后建一个补丁分支:修掉除法,加第四条测试,同时放松两条已有断言。新加的测试用的是 pages(10, 5) == 2,这个断言在有 bug 的代码上照样通过,所以它什么也没抓住。
第二步:三状态脚本
脚本假设已安装 pytest,并假设 src/ 是唯一承载修复的目录。两个假设都可以调整,局限部分会回头讲。脚本用 git worktree add --detach 分别拉出 base、patched、mutant 三个工作树;mutant 树建好后,用 git checkout BASE_REF -- src/ 把源码退回基线,测试不动。
然后依次跑三遍套件,把输出写进同一份报告。基线若是全绿,脚本给出警告:缺少失败的复现。打补丁状态若出现失败,脚本判定补丁没能让套件通过。变异状态若没有失败,脚本判定套件在没有修复的情况下照样通过,测试什么也没观察到。最后统计测试目录里新增和删除的断言行数,只要删除数大于零,就提示先读测试 diff。
第三步:像评审者一样读输出
在夹具上运行脚本,报告大致如下:基线状态一条失败、三条通过;打补丁状态四条全过;变异状态四条全过,脚本判定失败,因为套件在没有修复时也通过。断言变动统计显示新增一条、删除两条,并提示先看测试 diff。
两个独立信号指向同一处薄弱:变异运行证明新测试分不清修复与损坏的代码,变动计数显示两条早先的保证被放松了。单独看,两个信号都不足以定论;合在一起,就足以支撑一次“请求修改”而不是批准。
失败信息本身和失败同样重要。变异运行如果是因为导入错误而失败,那它对断言什么也没证明,所以结果算数之前,要先在日志里确认出现的是 AssertionError 或那条具名测试。
免费模型能帮哪一段,不能帮哪一段
免费模型适合这个循环里偏文本、低风险的部分,但它替代不了维护者的判断。实践中站得住的分工是这样的:
- 模型可协助:把失败的变异日志改写成两句 PR 评论,从 diff 起草缺失的测试,说出补丁改变了哪些可观察行为,提出重构合理触及了哪些已有断言。
- 只能由维护者做:判断一条被放松的断言是否正当,判断套件是抖动还是真的错了,以及批准合并。
把模型限制在这个边界内的提示骨架是:给定统一 diff、基线测试日志、打补丁测试日志和变异测试日志,不要提出合并决定;列出 diff 改变的可观察行为;对每条新测试说明它在没有 diff 时会不会失败;提出一条在变异状态下会失败的额外测试;凡是无法从日志判断的,标为未知。
通过 MonkeyCode 的免费模型访问可以覆盖上面的起草步骤,不需要付费密钥;免费服务器选项则给三个工作树一个一次性主机,而不是占用评审者的笔记本。需要披露的是:本文是作为 MonkeyCode 产品推广的一部分准备的。免费模型访问和免费服务器选项的容量与条款由运营方提供,可能变化,在团队流程里依赖它们之前,请核对当前公布的条款。
一次性主机是实打实的便利,不是营销话术。三个工作树加一个虚拟环境再加测试缓存,积累起来很快;报告写完就能销毁的主机,让“从干净状态开始”变成默认而不是苦差。在那里跑脚本、只把报告日志拷回来,评审产物就小而可审计。
命令大致是:克隆自己的 fork,抓取 PR 的 head 到本地分支,然后用 PY=python3 bin/three-state-check.sh origin/main pr-1234 > report.txt 2>&1 || true 跑一遍。
局限与失效模式
变异运行是启发式的,会在可预期的地方退化,评审者应当提前想到:
- 回退 src/ 可能破坏导入,变异失败可能来自被测断言之外。
- 对 mock 或日志字符串做断言的套件,在变异状态下常常照样全绿,即使行为已经变了。
- 抖动测试会让第三个状态变得嘈杂,信任这个信号之前需要重试策略。
- 大型单体仓库需要按受影响测试做选择,这会缩小变异运行的覆盖范围。
- 合理重命名或重构测试的补丁会触发变动计数,需要人工阅读。
没有可用测试命令的仓库不该从这里起步,脚本只是放大一个已经存在的套件。把测试当文档而不是约束的团队,会发现第三个状态长期是红的,一周之内就会把它关掉。一个月只评审一个小补丁的维护者,仔细读测试 diff 比维护三个工作树和一个 shell 脚本更划算。
这套流程在补丁量超过评审注意力、且被放松的断言发现得越晚代价越高的仓库里才回本。三个工作树、一个脚本、一份日志,足以让这项检查变成例行公事。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.