
你有没有遇到过这种情况:AI 编程工具几分钟改完一堆代码,看起来挺靠谱,但心里总不踏实——测试套件全绿,可你知道,那些测试是改之前写的,根本没覆盖这次改动。
最近 Meta 发了一篇论文,讲 JiTTesting(Just-in-Time Catching Test Generation),我觉得思路很值得分享。核心做法是:每次 PR 提交时,让 LLM 临时锻造一套测试,专门抓『这次 diff 刚引入的行为差异』,抓完就退场。不取代你现有的测试,而是在它上面加一道动态防线。
当 Agent 和 Copilot 成为日常开发工具,传统测试体系有三个问题越来越明显:
1. 维护成本爆炸
AI 提交的 diff 又大又频繁,人提前写的单元测试第二天就过时。如果每一条 AI 生成的用例都当永久资产维护,人力成本根本扛不住。脆断断言满天飞,维护倒比写代码还累。
2. 传统加固测试「只守未来,不抓当下」
Hardening Test(加固测试)的设计目标是:生成时通过 → 入库 → 长期防回归。它天然对『本次 diff 刚引入的行为变更』不敏感——旧套件全绿,不代表新代码没埋雷。它设计出来就不是干这个活的。
3. 全量补静态测试不划算
AI 产量越高,静态套件越容易脆断。而且那些埋得深的逻辑变更,常规功能测试未必能揪出来。你花大力气补了一堆测试,结果发现覆盖率上去了,但真正该抓的 bug 一个没抓到。
核心矛盾很简单:代码变更速度远大于人类写测试的速度,而且传统测试默认『代码是对的』,不会主动去找当前 diff 里的意外行为差异。
JiTTesting 的核心反转是:把测试从『统一当固定资产』拆成两条线,并存,互不替代。
Hardening Test(永久资产) | Catching Test(临时探针) | |
|---|---|---|
目的 | 防未来回归 | 抓本次 diff 的行为差异 |
生命周期 | 入库,长期维护 | 默认不入库,用完即弃 |
通过条件 | 生成时通过 | 设计上期望在 diff 上失败 |
关键概念是 Weak Catch 和 Strong Catch。同一测试在旧版本 parent 上通过、在新版本 diff 上失败,这叫 Weak Catch(弱捕获),只说明两边行为不一样。再经过通用预言判断:如果这个失败确实是 bug → Strong Catch(强捕获/真阳性);如果只是测试本身较真或是预期内的变更 → 假阳性。
还有个容易误解的点:Passing Test 可以「收割」为永久资产。
Catching 流程里,LLM 生成的测试会在 parent 和 diff 上各跑一遍。那些在两个版本上都通过的测试,说明它不是专为制造失败而写的,而是正常覆盖了行为。这类测试进入 passing 池,列为 Hardening Candidate(候选加固测试),经人工评审稳定性、长期价值、维护成本后,值得的才固化入库。不是 parent 通过就自动入库,但也不是全部丢弃。
永久 Hardening 资产继续存在;每次 PR 额外由 LLM 锻造一套一次性 Catching 探针抓差异,抓完就退场,只把少量顺手写得好、覆盖有价值的 passing 测试评审后转永久。
Dodgy Diff 管线(铺量型) 不猜意图,把 diff 直接当『嫌疑变异体』,LLM 生成测试来区分 parent 和 diff 的行为。简单粗暴,覆盖广,但误报也多。
Intent-Aware 管线(精准型) LLM 先读 diff 标题、描述、代码 → 推断业务意图 → 列出风险点(空值、边界、状态机)→ 在 parent 上注入变异(比如把 >= 500 改成 > 500、注入 return null、条件取反等 6 类标签)→ 生成『parent 过、mutant 挂』的测试 → 再拿去跑真实 diff。
Meta 的实测数据挺有意思:Intent 管线生成 811 条测试,52 个 weak catch(率 6.4%),覆盖 7.9% 的被测 diff;Dodgy 生成 1621 条,41 个 weak catch(率 2.5%),覆盖 4.0%。Intent 的捕获率大约是 Dodgy 的 2.6 倍。
RubFake 规则评估器(确定性信号层) 识别 14 类误报模式(布尔翻转、mock 错误、反射断裂等)和 26 类风险标记(扫描执行结果与代码片段),标记可疑度,但不单独定论。
LLM-as-Judge 集成(语义评分层) 多模型输出风险概率分(-1 到 +1)加等级加文字理由,聚合排序,减少人工评审量。
Human-in-the-Loop(最后一道关) 只给工程师发一句是非题——『这个行为变化是你预期的吗?』预期内点 dismiss,非预期就修代码。不让人读测试代码,几秒钟完事。
第 1 步:环境隔离。CI 拉出 parent(master 基线)和 diff(待提交版本)两套工作副本,测试跑独立子进程,避免 APM 探针或自定义 ClassLoader 污染。
第 2 步:Diff 解析。提取变更函数/类,过滤无关文件,输出结构化 diff 摘要,喂给后续两条管线。
第 3 步:双管线并行生成。Dodgy 直接针对 diff 行为差异写测试;Intent 推意图 → 列 risks → 造 mutant → 生成『parent 过、mutant 挂』的测试。
第 4 步:双版本验收。所有候选测试先跑 parent,必须全过(不过的说明测试本身有错,丢弃)。再把同一批测试跑 diff。仅『parent 过 + diff 挂』的留为 weak catch;parent 和 diff 都过的进入 passing 池,作为 hardening candidate 候选。
第 5 步:降噪排序。RubFake 打确定性规则信号 → LLM 集成打语义分 → 合起来排优先级,只把高分 weak catch 推给人审。
第 6 步:人审触达。工程师收到类似『布尔值从 true 变 false 是否预期』的提问,几秒到几分钟可完成 dismiss。Catching 测试本身默认不入库,不用担心测试垃圾堆积。
第 7 步:选择性收割。Passing 池里断言稳定、覆盖有价值、维护成本可控的,人工评审后转为 Hardening Test 正式入库。当前工程实现默认只留归档目录,不自动 push。
从 0 到 1 分四阶段推进:先选一个项目跑 Dodgy 管线看 weak-catch 率和单次成本;再铺自己项目的 RubFake 规则库校准误杀率;接着接入单模型语义评分,确认互补后升级多模型集成;最后上 Intent 三侧矩阵(parent/mutant/diff),并把 passing test 的收割流程跑通。别一上来就追求全自动入库。
JiTTesting 给传统测试体系补了一条按 diff 定制的动态防线。AI 写码越快,这条防线越值钱——但它不取代永久测试资产,而是让『临时探针』和『永久加固』各司其职。
在 AI 写代码越来越快的今天,这个思路挺值得工程团队认真考虑一下。