作者认为免费AI算力最值得用于变异测试而非生成更多测试用例或功能代码,因为变异分数才能真正衡量测试套件捕获回归缺陷的能力。
行覆盖率告诉你哪些代码执行了,而不是你的测试实际上能 catch 到哪些失败,而这个差距正是大多数免费 AI 算力被悄悄浪费的地方。团队用免费的模型额度生成额外的测试或额外的功能,但这两件事都不会移动那个预测回归存活率的数字:突变分数。突变测试向一个模块注入故意的缺陷,然后检查现有测试套件能否杀死它们,这使得它成为在工作团队中使用免费模型访问的最高杠杆用法。原因在于结构层面——因为突变体生成是机械化的、高吞吐的、低风险的,这正是免费模型处理得很好而付费 API 不应该碰的工作负载。
覆盖率是代码行的 popularity contest,而不是断言强度的度量,一个从不失败的测试无论它访问了多少分支,都只是装饰。一个测试套件可以达到百分之九十的行覆盖率,但仍然漏掉那个把支付端点搞坏的 off-by-one 错误,因为覆盖率从来不问断言是否真的会触发。突变分数直接问了这个问题——改变被测代码,观察套件是否注意到。这是一个和其他 AI 时代指标一样存在的测量谬误:一个数字被信任是因为它容易计算,而不是因为它预测了你关心的结果。只有后者才能预测一个回归是否会存活到生产环境。
突变体生成有三个特性,使它非常适合免费模型计算。第一,它是机械化的——一个好的突变体是单一的最小编辑,比如翻转一个比较、交换一个布尔值、或者删除一个分支。第二,它是高吞吐的——因为统计置信度需要每个模块数百个突变体,而不是手工挑选的十几个。第三,它是低风险的——因为一个编译失败或保留行为的突变体直接被丢弃,没有突变体会发布。
MonkeyCode 的免费模型访问改变了这个实验的经济学,因为突变工厂可以运行数百次生成而不会产生计量账单。披露:本文是 MonkeyCode 产品推广的一部分。免费服务器选项然后给测试 harness 一个一次性的地方来并行执行每个突变体对套件的测试,这让工作负载远离你的生产 CI runner 和笔记本。这个组合把突变测试从季度审计变成了任何团队都可以在一个周五下午运行的常规检查。
下面的 harness 有意做得很小,因为目标是测量一个模块,而不是构建一个平台。第一步是挑选一个模块及其现有测试文件,第二步是用免费模型使用严格的提示模板生成突变体。第三步是在免费服务器上并行运行每个突变体对套件的测试,第四步是将每个幸存者分类到三个桶中。第五步是将每个存活的突变体转化为回归测试,这闭合了循环并使下一次分数更高。
提示模板强制每次调用一个突变体,因为单一的最小编辑才能使测量可解释:
You are a mutation-testing assistant. Given the function below, produce one
mutant: a single minimal edit that changes observable behavior. Use exactly one
operator: flip a comparison, swap a boolean, change a boundary (<= to <),
delete a branch, or replace a constant. Output only the complete modified
function with no explanation.
为了规模化,每次调用批量处理五个函数并请求五个突变体,然后在你控制的分隔符上分割输出。下面的 harness 假设突变体已经物化为每个突变体一个目录,每个目录包含目标文件的一份副本:
# mutant_harness.py — execute every mutant against the target suite
import json
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
TARGET = Path("payment.py")
SUITE = ["-m", "pytest", "tests/test_payment.py", "-q"]
MUTANTS = Path("mutants") # one subdirectory per mutant id
def run_one(mutant_id: str) -> dict:
original = TARGET.read_text()
mutant_src = (MUTANTS / mutant_id / TARGET.name).read_text()
TARGET.write_text(mutant_src)
try:
result = subprocess.run(
[sys.executable, *SUITE], capture_output=True, text=True, timeout=120
)
killed = result.returncode != 0
except subprocess.TimeoutExpired:
killed = False
finally:
TARGET.write_text(original)
return {"id": mutant_id, "killed": killed}
def main() -> None:
mutants = [p.name for p in MUTANTS.iterdir() if p.is_dir()]
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(run_one, mutants))
score = sum(r["killed"] for r in results) / len(results)
print(json.dumps({"score": round(score, 3), "results": results}, indent=2))
if __name__ == "__main__":
main()
在免费服务器上运行它并将报告重定向到文件:
python mutant_harness.py > mutation_report.json
Harness 将任何非零退出计为 kill,这对测量工具来说是保守的方向,因为一个让 runner 崩溃的突变体仍然是套件注意到的一个行为变化。它假设单个文件目标、一个 pytest 套件、以及每个突变体 120 秒超时,所以请将超时调整为你的最慢测试,并将 worker 数量保持适度,直到你观察到服务器的真实限制。
分数是套件杀死的突变体比例,它映射到一个具体的行动。
低分不是失败报告;它是一张精确的地图,告诉你哪些断言在说谎。高分不是停止测试的许可证;它是你可以将下一轮免费计算花在别处的许可。
三个注意事项保持这个工作流的诚实。等价突变体——即尽管有编辑但保留行为的突变体——会膨胀分母,需要在信任分数之前做一个快速的分类传递。Flaky 测试会污染结果,因为一个超时或与 flaky 测试碰撞的突变体会被记录为幸存者,即使套件本可以 catch 到它。而且 harness 假设你的套件足够快以运行数百次,所以有长达数小时测试套件的团队应该在花费任何计算之前将范围缩减到一个模块。
没有测试套件的团队、有明显 flaky CI 的团队、以及无法区分等价突变体和真正幸存者的团队应该完全跳过这种方法。向一个从不失败的套件注入缺陷会产生零分和没有新信息,所以这些团队的第一步是基本的断言审计,而不是突变 campaign。
突变分数是唯一衡量套件失败能力的指标,而免费模型计算非常适合产生揭示它的缺陷。把额度花在突变体上,在免费服务器上运行它们,让幸存者告诉你哪些断言在说谎。这比另外一百行生成的功能代码有更好的回报,而且这是一个你可以在评审中辩护的测量。