想知道 AI 写的测试到底能不能失败,就故意把它守护的代码改错,再跑一次。如果测试仍然通过,说明它并没有保护这段行为,整套测试再绿也没用。测试通过只说明测试和当前代码彼此一致。

今天就能手动做:在测试声称守护的那一行引入一个小错误,只重跑这个测试,确认它因为行为断言而失败;然后恢复代码,确认它重新变绿。Stryker、PIT、mutmut 这类变异测试工具,就是把同一件事自动化,批量应用到许多小改动上。

要点

  • 全绿不是证据。有用的测试,在它描述的行为被破坏时必须失败。
  • 最快的检查:改坏被守护的那一行,只重跑这个测试,期待断言失败,再恢复。
  • 崩溃、导入错误、编译错误,都不算测试抓到了缺陷。
  • 把期望值改反,只能证明断言被执行了,证明不了测试能发现错误代码。
  • 变异测试把「改坏再看」自动化。存在等价变异体,100% 并不总能达到。

为什么全绿的 AI 测试可能毫无价值?

因为在正确代码上通过的测试,在错误代码上也可能照样通过。覆盖率补不上这个缺口:PIT 文档指出,行覆盖率只衡量哪些代码被执行,不衡量测试能否发现其中的错误。

生成的测试里值得留意的模式:

  • 只断言形状: 不为空、类型对、没抛异常,但从不检查值。
  • 期望值抄自代码: 用实现本身的公式算期望值,或把当前代码的输出直接粘进来。这样测试会和缺陷保持一致。
  • 把被测单元 mock 掉: 断言检查的是 mock,不是函数本身。
  • 断言调用而非结果: 「save() 被调用了」即使数据没存对也能通过。
  • 只测正常路径: 一个典型输入,没有边界或拒绝用例。
  • 根本没运行: 被跳过、过滤或没被收集的测试不等于通过。要看报告的用例数,不只看退出码。

不用专门工具,怎么确认测试能失败?

在测试对应的生产代码里故意制造一个错误,并要求测试因此失败。

  1. 从干净状态开始。 先 commit 或 stash,确保能原样恢复。记下测试名和它应守护的那一行。
  2. 只改坏一处: >= 改成 >、- 改成 +、换一个常量,或提前返回错误值。保持代码能编译。
  3. 只运行这一个测试,用测试运行器常规的单测选择方式。
  4. 读失败信息。 应该是关于行为的断言失败(值不对、状态不对)。语法错误或导入错误说明检查根本没发生,改得更小一点再试。
  5. 恢复后重跑。 确认 diff 已干净,同一个测试重新通过。
  6. 记一行: 测试名、改了什么、失败信息、版本。

如果是缺陷修复,更干净的做法是把新测试放到修复前的提交上跑:那里应失败,在最终 diff 上应通过。如何验证 AI 生成的缺陷修复 详细介绍了这种先红后绿的记录方式。

把期望值改反(== 60 改成 == 61)是更弱的检查。它能发现被跳过或没被收集的测试,但你改的是测试而不是代码,所以无法说明错误的实现会不会被发现。

一个小教学练习

仅用于教学,不是 Boloop 的测试套件。弱测试只查形状;强测试按规则检查值(80 减去 25% 等于 60)。保存为文件后用 python3 运行。

def apply_discount(price, percent):
    return round(price * (1 - percent / 100), 2)

def apply_discount_mutant(price, percent):
    return round(price * (1 + percent / 100), 2)  # deliberate bug

def test_weak(fn):
    result = fn(80, 25)
    assert result is not None
    assert isinstance(result, float)

def test_strong(fn):
    assert fn(80, 25) == 60.0

def check(test, fn):
    try:
        test(fn)
        return "pass"
    except AssertionError:
        return "FAIL"

for name, fn in [("original", apply_discount), ("mutant", apply_discount_mutant)]:
    print(f"{name}: weak={check(test_weak, fn)} strong={check(test_strong, fn)}")

2026-11-08 用 Python 3.13 运行的输出:

original: weak=pass strong=pass
mutant: weak=pass strong=FAIL

两个测试在正确代码上都是绿的,只有一个发现了缺陷。

变异测试什么时候有用,什么时候多余?

当你希望「改坏再看」覆盖许多行,而不只是你手挑的那一行时,它有用。这类工具会插入小错误(变异体),对每个变异体运行测试,报告有测试失败(killed,被杀死)还是全部通过(survived,存活)。

  • [Stryker](https://stryker-mutator.io/docs/): StrykerJS(JavaScript/TypeScript)、Stryker.NET(C#)、Stryker4s(Scala)。
  • [PIT](https://pitest.org/): Java 与 JVM。它变异字节码;可从命令行、Ant 或 Maven 启动,Gradle 由第三方组件集成。
  • [mutmut](https://mutmut.readthedocs.io/en/latest/): Python。mutmut run 会调用 pytest,mutmut browse 查看变异体。需要支持 fork 的系统,所以在 Windows 上要在 WSL 里运行。把变异体写回磁盘前先提交代码。

按同一批文档解读结果:

  • Survived(存活) 指测试跑过了被变异的代码却没有失败;No coverage(无覆盖) 指根本没有测试执行到它。
  • Stryker 不把运行时错误和编译错误的变异体计入变异分数。
  • 等价变异体 行为与原代码相同,任何测试都杀不死。Stryker 表示它没有确定的办法找出它们。不要追求 100%。
  • 成本: PIT 建议经常运行,但只针对改动过的代码;Stryker 的 FAQ 建议只变异必要的部分。

对一次性脚本、还跑不稳定的测试套件,或者风险逻辑只有几行、手动就能查完的情况,它属于多余。先修好不稳定和被跳过的测试。

适合谁?

适合要验收 Claude Code、Codex、Copilot 或类似智能体所写测试的开发者和审查者,尤其是随修复或新规则一起加进来的测试。

这不是完整的测试策略,替代不了真实用户路径检查、安全审查或性能测试。能失败的测试仍可能断言了错误的规则;期望值必须来自约定的需求。

和 Boloop 其他指南的关系

它展开的是其中一步。AI 生成代码审查与验收清单 把「证明测试能失败」列为四项 AI 专属检查之一,本文讲怎么做。如何验证 AI 生成的缺陷修复 把它用作先红后绿。如果要让智能体设计用例或准备红测试交接,test-case Skill 把这些模式分开处理。

常见问题

代码覆盖率高就够了吗?

不够。覆盖率只说明哪些行被执行;没有有效断言的测试同样能覆盖它们。

能让智能体自己做失败检查吗?

可以先让它做,然后你在最终 diff 上自己重跑,并阅读失败信息。智能体的总结不是测试结果。

改坏代码后测试因导入错误失败,算吗?

不算。它没走到断言。换一个更小、不影响代码加载的改动。

变异分数应该定多少?

本文读过的文档没有给出通用目标。把改动代码里的存活变异体当作审查事项。

来源

  1. Stryker 文档:What is mutation testing?、Mutant states and metrics、Equivalent mutants、FAQ。
  2. PIT:首页、Quickstart、Basic concepts、Mutators。
  3. mutmut 文档.

局限

Boloop 没有为本文运行 Stryker、PIT 或 mutmut,也不报告任何变异分数。工具相关说法来自上述官方文档(2026-11-08 阅读),不同版本行为可能变化。唯一执行过的代码是上面的教学练习,输出如文中所示。