"帮我写点测试"最容易翻车的地方
把一个函数丢给 LLM,让它"生成单元测试提升覆盖率",最常见的结果不是测试写得不够好,而是测试写得看起来没问题,实际什么都没测:
def test_calculate_discount():
result = calculate_discount(100, 0.1)
assert result is not None # 这种断言,永远不会失败
def test_process_order():
# 编译不过:Mock 的返回值类型和实际接口不匹配
mock_client = Mock(return_value="success")
process_order(mock_client)这类测试有覆盖率数字(甚至能让覆盖率报告看起来"变好了"),但对代码质量没有任何保护作用——assert result is not None 永远不会失败,等于什么都没断言。更麻烦的是另一类问题:LLM 生成的测试有时候连编译/运行都过不了,直接被扔进 CI 只会制造新的红叉。
Meta 在 2024 年发的论文《Automated Unit Test Improvement using Large Language Models at Meta》要解决的正是这个问题,工具叫 TestGen-LLM。它的核心思路不是"训练一个更聪明的模型来写测试",而是反过来:假设 LLM 的产出大概率是垫圾,搭一套机械化的过滤器,把不合格的东西全部筛掉,只留下能证明自己有效的那一小部分。
TestGen-LLM 的四层过滤器
论文把这套方法称为 Assured Offline LLM-Based Software Engineering(Assured LLMSE):LLM 只是流水线里的一个生成步骤,它的产出不会被直接信任,而是要经过一系列可验证的检查,只有全部通过才会被推荐给人。这个"assured"(有保障的)的关键词,就是区别于"直接把模型输出当结果用"的地方。
具体是四层过滤,逐级淘汰:
LLM 生成候选测试
↓
① Build Filter(编译过滤)
候选代码必须在现有工程基础设施下能完整编译通过
编译不过 → 直接丢弃
↓
② Pass Filter(首次执行过滤)
编译通过的测试必须首次运行就通过
——这一步不去判断"失败是不是发现了真 bug",
因为目前没有自动化手段区分"真 bug"和"断言写错了",
所以统一采取保守策略:首次不过就丢弃
↓
③ Flakiness Filter(稳定性过滤)
连续运行 5 次,必须每次都通过才算稳定
只要有一次失败,就判定为 flaky,丢弃
↓
④ Coverage Filter(覆盖率过滤)
必须证明自己确实提升了代码覆盖率
不提升覆盖率的测试,即使前三层都过了,也丢弃
↓
只有走完全部四层的测试,才会被推荐给工程师 review四层过滤器的设计哲学是层层收窄,宁可漏掉有价值的测试,也不能让垫圾测试流出去。特别值得注意的是第三层——连续跑 5 次才能过滤 flaky 测试,这个做法后续也被 Qodo Cover 的路线图直接借鉴(下文会提到)。
一个容易被误解的地方需要澄清:TestGen-LLM 的过滤器里没有用到变异测试(mutation testing)。 论文里提到变异测试可能是理论上覆盖率评估效果最好的标准,但明确把它归为未来工作,原因是"在 Meta 需要的规模下,这类计算量巨大的技术难以部署"——换句话说,变异测试的思路是对的,但工程上跑不动,所以这一版用的是更轻量的"覆盖率是否提升"作为过滤标准,而不是"每一行代码的变异体有没有被杀死"这种更精确但更贵的指标。
真实数字:这套方法在 Meta 内部跑出了什么效果
论文给出了两组数据,分别对应"实验室评测"和"真实工程师使用"两个场景,值得放在一起看,因为差距本身就很有信息量。
实验室评测:Instagram 的 Reels/Stories 组件
在 86 个 Kotlin 组件(31 个 Stories、55 个 Reels)上测试:
75% 的测试类:至少生成了一个能正确编译的新测试用例
57% 的测试类:编译通过 + 稳定通过(走完前三层过滤)
25% 的测试类:编译通过 + 稳定通过 + 确实提升了覆盖率(走完全部四层)这组数字说明一个很实际的问题:"LLM 能不能生成测试"和"LLM 能不能生成有价值的测试"之间有巨大的衰减。 四分之三的组件能拿到一个"看起来能编译"的测试,但只有四分之一真正提升了覆盖率——中间三层过滤器砍掉的,恰恰是"看起来有用但实际没用"的那部分产出。
真实场景:三次 Test-a-thon(测试冲刺活动)
比实验室数据更有说服力的是三次内部 test-a-thon 的真实使用数据:
生成的 diff 工程师接受 接受率
─────────────────────────────────────────────────────
Instagram(第一次) 17 16 94%
Instagram(第二次) 42 36 86%
Facebook 280 144 51%(review 过的里接近 70%)
─────────────────────────────────────────────────────
合计 1979 个测试类中,196 个被成功改进(约 10%)
73% 的改进建议被工程师接受落地两组数字放在一起对比会发现:实验室里"能通过全部过滤器"的比例大约是 25%,但真实场景里"工程师愿意接受"的比例是 73%。这不矛盾——过滤器筛掉的是机械层面不合格的产出(编译不过、不稳定、不提升覆盖率),而"工程师接受"衡量的是在通过了机械过滤之后,这个测试是否真的值得合并进代码库。也就是说,四层过滤器解决的是"不要让垫圾流出去",不是"生成的东西一定有价值"——后者仍然需要人来判断。
开源实现:Qodo Cover 怎么把这套思路落地成 CLI 工具
TestGen-LLM 本身是 Meta 的内部工具,没有开源。Qodo Cover(前身是 Codium 的 Cover-Agent)是这套思路第一个公开的开源实现,可以直接跑在自己的项目上。
它的架构把 TestGen-LLM 论文里的"过滤器"概念,落地成了四个具体组件:
Prompt Builder(提示构建器)
从代码库里收集必要信息(源文件、已有测试、覆盖率报告),拼装成给 LLM 的 prompt
↓
AI Caller(模型调用器)
基于 prompt 调用 LLM 生成候选测试(通过 LiteLLM,可换不同模型厂商)
↓
Test Runner(测试执行器)
实际跑一遍项目的测试命令,执行新生成的测试
↓
Coverage Parser(覆盖率解析器)
解析覆盖率报告(Cobertura/JaCoCo 等格式),验证覆盖率是否真的提升了
↓
通过 → 保留测试,继续下一轮迭代
不通过 → 丢弃,重新生成候选这本质上是 TestGen-LLM 的 Pass Filter + Coverage Filter 的具体实现(Build Filter 隐含在 Test Runner 执行失败里,Flakiness Filter 是路线图里计划加入、明确提到要"参照 TestGen-LLM 的做法跑 5 次"的功能)。
使用方式是一个 CLI 循环:指定源文件、测试文件、覆盖率报告路径、目标覆盖率百分比(--desired-coverage)和最大迭代次数(--max-iterations),工具会反复生成候选测试、跑测试、检查覆盖率,直到达到目标覆盖率或用完迭代次数。这个循环可以直接接入 CI 流程,也可以本地跑"一键提升这个模块的覆盖率"。
需要指出的是,Qodo Cover 这个开源项目在 2025 年中之后已经不再维护(README 里明确标注"no longer maintained",建议想继续开发的人自行 fork)。这不影响它作为"TestGen-LLM 思路落地参考"的价值,但如果要在生产环境依赖它,需要考虑维护性风险——这也是本系列后面几篇会反复遇到的问题:开源验证性项目和生产可用工具之间,往往还有一段距离。
这套方法的真实边界
把 Meta 的实验室数据和 test-a-thon 数据放在一起看,可以推出这套方法真实好用的场景边界:
好用的场景:逻辑相对独立、依赖关系清晰的模块——LLM 容易根据函数签名和现有测试风格推断出合理的输入输出关系,四层过滤器能有效筛掉大部分垫圾产出。
不好用的场景:
- 依赖大量外部状态或复杂 Mock 的代码——LLM 生成的 Mock 经常跟真实接口的行为不一致,容易被 Build Filter 或 Pass Filter 筛掉,存活率低
- 需要深层业务语境才能写出有意义断言的代码——测试可能"能跑通"但断言写得没有意义(比如开头例子里的
assert result is not None),而覆盖率过滤器只看代码是否被执行到,不会判断断言本身有没有意义,这是四层过滤器本身没有解决的盲区 - 对确定性和可复现性要求极高的场景——flakiness filter 只跑 5 次,统计上仍然可能漏判本质上不稳定的测试
最后一点值得强调:四层过滤器保证的是"这个测试机械上是合格的",不保证"这个测试的断言在业务语义上是正确的"。 覆盖率提升是一个可以自动化验证的代理指标(proxy metric),但"这个测试真的测到了我关心的东西"仍然需要人工 review——这正是为什么 Meta 内部数据里,"通过全部过滤器"和"工程师接受"是两个不同的、都需要单独考察的数字。
总结
- LLM 直接生成的测试大概率是"垫圾"——编译不过、断言写死、或者根本不提升覆盖率,TestGen-LLM 的解法不是让模型更聪明,而是用四层机械化过滤器(编译、首次执行、稳定性、覆盖率)逐级淘汰不合格产出
- 变异测试不是 TestGen-LLM 当前用的方法,论文明确把它列为受限于计算成本、尚未落地的未来方向,当前用的是更轻量的覆盖率提升作为过滤标准
- 实验室数据(25% 走完全部过滤器)和真实场景数据(73% 被工程师接受)衡量的是两件不同的事——前者是机械合格率,后者是人工价值判断,四层过滤器只解决前者
- 开源实现 Qodo Cover 把这套思路落地成了 CLI/CI 可用的迭代循环,但项目本身已停止维护,落地时需要评估维护性风险
欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页