AI 生成的测试,如何证明真能抓住错误
文章提出模型生成性质测试的准入流程:锁定测试夹具、审查源码、用变异验证失败能力,再检查重复执行的一致性和运行耗时。代码尚未执行,决策表数据是假设值,应作为方案参考。
AI 深度解读
模型一分钟能写出十条属性检查,但全绿不等于能发现问题。作者提出了一套准入流程:冻结测试夹具、扫描源码、注入缺陷、重复运行,再限制耗时。重点是证明模型生成的检查有资格进入合并门禁。
这套方案值得读,因为它把「测试写得快」和「测试值得信任」拆开了。不过,原文代码没有实际执行,涉及的决策数字也是假设值。它提供的是一套可审查的工程思路,还不是经过验证的效果报告。
测试全绿,为什么仍然可能放过错误
作者列出了三种容易制造安全感的检查。
第一种是断言没有区分能力。比如 assert result is not None,在某些业务里确实有意义,但如果正常实现和错误实现都能返回一个对象,它就发现不了行为偏移。至于 assert True,更是永远通过。
第二种是测试依据被一起改松了。模型新增检查时,也可能把精确比较改成近似比较、扩大 mock 返回范围,或者重新生成作为预期结果的 golden file。检查通过了,却无法证明原来的行为约束仍然成立。
第三种是不稳定检查,也就是 flaky test。同一份代码有时通过、有时失败,团队逐渐学会「再跑一次」。重跑到绿是在筛选有利结果,不能算验证成功。
因此,作者先冻结测试夹具(fixture),也就是测试依赖的固定输入和预期数据。生成前后分别遍历 tests/fixtures/**/*,用 hashlib.sha256 计算文件内容哈希,生成路径到哈希的清单。对比两份清单,可以发现文件新增、删除和内容变化。
流程约束比脚本更重要:模型生成检查期间,夹具必须保持不变;修改夹具要单独提交,并明确由谁负责。发现漂移后,整批候选检查都需要重新审视,因为已经无法确定它们依据的是哪套标准。
检查和它用来证明正确的数据,不能在同一批生成过程中悄悄互相迁就。
怎样证明一条检查真的能抓住错误
冻结夹具之后,作者先用 Python 的 ast.parse 和 ast.walk 扫描源码,但只把明确无效的情况直接拒绝。
示例中,HARD_REJECT = {"assert_const"} 对应 assert True、assert 1 这类常量断言。assert_only_not_none 和 skip_or_xfail 只产生提示,分别指向非空判断,以及 skip、xfail 调用。
这里的分寸很关键:非空检查可能确实表达了业务约束,不能只凭语法形式判死刑。静态扫描负责找疑点,下一步才检验检查有没有发现缺陷的能力。
作者采用变异测试(mutation testing):人为给被测模块植入一个小错误,例如翻转比较符、制造边界偏差、提前返回。如果候选检查能因此失败,就说它「杀死」了这个变异体。
下面这段示意代码通过环境变量选择变异体,再逐个运行候选检查:
def mutant_kills(candidate: str, mutants: list[str]) -> int:
kills = 0
for name in mutants:
env = {**os.environ, "MUTANT": name, "PYTHONHASHSEED": "0"}
r = subprocess.run(
[sys.executable, "-m", "pytest", candidate, "-q", "-x"],
env=env, capture_output=True, text=True,
)
kills += int(r.returncode != 0)
return kills
要看的是最后一行:这里把所有非零退出码都计成一次击杀。作为示意容易理解,但真正落地时,必须区分断言发现错误与导入、收集、运行环境故障,否则基础设施坏了也可能被算成测试有效。
变异体由 conftest.py 在导入时根据 MUTANT 应用。作者特别强调,遇到未知的 MUTANT 值必须报错。拼错名称却静默运行原实现,会让团队误以为检查抓不住缺陷,实际是缺陷根本没注入。
变异体集合也要像测试代码一样接受审查。它决定了这套验证能够覆盖什么。
零击杀只能叫「尚未证明有效」。如果变异体没有触及某条业务约束,相应检查当然可能全部通过。作者建议把这类候选连同变异体清单交给人工复核,而不是删除。
稳定性和耗时,也要算进准入条件
能抓住人为缺陷,还不够。候选检查也必须在正常代码上稳定运行。
作者建议在没有注入变异的代码树上运行三次,固定 PYTHONHASHSEED="0" 和应用种子 PROP_SEED="1234",比较退出码:
def outcomes(candidate: str, runs: int = 3) -> set[int]:
seen = set()
for _ in range(runs):
r = subprocess.run(
[sys.executable, "-m", "pytest", candidate, "-q"],
env={**os.environ, "PYTHONHASHSEED": "0", "PROP_SEED": "1234"},
capture_output=True,
)
seen.add(r.returncode)
return seen
if len(outcomes("tests/props/candidate_a.py")) > 1:
print("candidate is flaky -> freeze the candidate, not the suite")
要注意 len(outcomes(...)) > 1:它只能发现三次运行之间的退出码不一致,不能证明候选全部通过。三次都失败也只有一个退出码,因此正式流程还需要单独确认正常实现上的检查成功。
另外,设置 PROP_SEED 只有在应用或测试代码实际读取它时才有效,原文没有提供接线实现。三次结果一致,也只是有限次数内未观察到波动。
发现不稳定后,作者主张冻结具体候选。它可以继续运行、提供信息,但不能作为补丁安全的证据。冻结整个测试套件,会掩盖究竟是哪条检查不可信,也容易让临时措施长期残留。
最后是按候选测量实际耗时,超过预算的转入夜间任务池。原文没有给出统一预算,只提出一个经验判断:每次补丁都要等待数分钟的门禁,很容易被绕过。
我的判断
我认同这套方案的方向:模型可以负责大量起草,准入判断交给可重复执行的程序。作者提到 MonkeyCode 的免费模型和服务器资源,同时披露文章属于其产品推广合作。这能解释候选生成和批量运行的成本话题,却不能证明准入方案有效,也不能据此认定免费额度长期可用。
变异测试能证明检查对某些错误敏感,不能证明它正确表达了业务需求。
模型可能照着当前实现写断言,连内部细节也锁死。这样的检查既能杀死变异体,也可能阻止一次正确重构。作者承认这一局限,并建议用重构试跑补充验证。
相比让人逐条审阅生成测试,这套流程更适合候选量较大、审核成本开始积累的场景。如果每个补丁已经有充分人工审阅,再维护变异体和运行框架是否划算,需要计算维护成本。作者自己也建议这类团队跳过。
原文没有执行示例代码,也没有提供缺陷检出率、误拒率或维护成本的对照数据。因此,「这些步骤值得尝试」有理由,「它比现有流程更好」仍是待验证的判断。
正式使用前,还要先验证准入框架自己:输入一个包含 assert True 的文件,应被静态拒绝;再输入一个已知能杀死特定变异体的检查,应恰好得到一次击杀,并在正常代码上三次返回同一退出码。框架没有接通,后面的分数就没有解释价值。
能拿来做什么
- 让 AI 批量生成测试前,先固定 fixture、mock 数据和快照。涉及预期结果的修改单独审查,避免新增测试顺手重写验收标准。
- 为确实需要自动筛选测试的模块,先整理一小组经过审阅的典型缺陷,再检验候选能否发现它们。零击杀时保留人工复核入口,不要把有限的缺陷集合当成完整需求。
- 在 Node 或前端测试项目里,可以迁移「正常实现先通过、注入缺陷后失败、重复运行再检查稳定性」的流程。原文实现依赖 Python 和 pytest,不能直接复制;种子控制、缺陷注入和退出状态分类都需要重新接线。
- 给候选检查设置独立的耗时预算和冻结状态,并用已知结果的样例验证整个准入流程。慢检查转入夜间运行,不稳定检查继续提供信息时,也要明确它不能支持合并判断。