外观
让 LLM 写单测接入 CI:AI 生成单元测试的工程化落地方案
问题背景:单测欠债的团队都卡在同一个地方
绝大多数团队都明白单测的价值,也都有过「这次一定要把覆盖率提上来」的决心,最后都死在同一个坎上:补测试的成本太高。一个两千行遗留 Service 类,人工补齐分支覆盖要两三天,业务迭代等不起,于是覆盖率永远停在 30%。LLM 改变了成本结构——生成一个分支覆盖良好的测试类只要几分钟,人负责评审与修正。但「让 AI 写单测」从个人 IDE 里的偶发动作,变成团队可信的工程设施,中间隔着流程设计:生成的测试进不进主干?谁来保证质量?覆盖率门槛怎么定才不会把存量欠债变成阻塞项?这些问题不解决,AI 单测很快就会沦为「跑一跑看个乐」的玩具。
方案对比:三种落法,门槛设计决定成败
IDE 内随手生成:开发者本地用 AI 编程助手对当前类生成测试,零流程成本,但质量完全依赖个人自觉,团队无法沉淀。PR 流水线自动生成:CI 检出本次变更的源文件,调用 LLM 批量生成测试、自动运行,通过后才允许进入评审,产出物是「已跑通的测试代码 PR」;这是工程化的主流形态。全量补齐专项:以季度为单位圈定核心模块批量补测,适合债务清理战役。关键分歧点在覆盖率门槛:直接设全局 80% 会让存量欠债瞬间阻塞所有 PR,正确姿势是「覆盖率棘轮」——只要求本次 PR 不降低整体覆盖率,存量欠债在棘轮约束下被新增代码的覆盖自然稀释。
实操步骤:四步搭起可信赖的流水线
第一步:PR 触发差异生成。 流水线检出 PR 相对主干的变更文件,只对被修改的源文件生成测试——全量生成的 token 成本高且与人工改动冲突率高:
yaml
name: ai-unit-test
on:
pull_request:
paths: ["src/main/java/**/*.java"]
jobs:
gen-and-verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: 对差异文件生成单测
# 生成脚本内部调用 LLM API,产出测试文件到 src/test 目录
run: node scripts/gen-tests.mjs --base origin/main --only-diff --out src/test
- name: 运行测试与覆盖率
run: mvn -B test jacoco:report
- name: 覆盖率棘轮校验
run: node scripts/coverage-gate.mjs --ratchet-file .coverage-ratchet.json第二步:提示词模板锁定质量要素。 模板里必须显式给出四样东西:被测类的完整源码、项目既有的测试风格示例(JUnit5 + Mockito 还是 TestNG)、禁止事项(禁止修改产品代码、禁止修改既有测试)、断言要求(每个测试方法必须有具名断言,禁止只跑不验):
text
你是资深测试工程师。为以下 Java 类生成单元测试:
- 框架:JUnit 5 + Mockito(参考既有测试风格:@ExtendWith(MockitoExtension.class))
- 覆盖要求:每个公开方法的正常分支 + 每个异常分支各至少一个用例
- 硬性要求:不得修改产品代码;每个测试必须有明确断言;
禁止使用 PowerMock;外部依赖一律 mock,禁止真实网络与数据库
- 输出:完整可编译的测试类,不要解释性文字
【被测类源码】{source}
【风格参考】{existing_test_example}第三步:人工评审清单。 AI 生成的测试不直接合入,评审人按固定清单过一遍:断言是否真的校验了业务结果(而不是 assertNotNull 万金油);mock 是否过度(连被测逻辑的核心协作者都 mock 掉,测试就只剩空壳);异常分支是否断言了异常类型与语义;测试之间是否相互独立、可并行执行。清单评审平均每文件 5 分钟,远低于手写成本。第四步:框架升级的回归用法。 大版本升级(如 Spring Boot 跨版本、JDK 升级)最大的恐惧是「不知道哪里会碎」——升级前用同一套流水线对核心模块批量生成测试并全部跑绿,形成行为快照;升级后重跑这批测试,失败项就是行为变更点。这把原本靠人工点检的升级回归,变成了可自动化的证据链,是 AI 单测投入产出比最高的场景。
第五步:流水线里的凭据治理不可忽视。 CI 调用生成 API 要用独立的低权限 Key(只开放所需模型、单独设用量上限),存放在 CI Secret 而不是脚本常量里;生成请求只包含被测类源码与风格样例,禁止把数据库配置、其他服务密钥等环境信息塞进提示词。被测代码会进入第三方模型的上下文,核心加密模块、合规敏感类应在配置中声明豁免清单、维持人工编写测试——这是 AI 进 CI 之前就该评审清楚的安全边界。
避坑总结
- 最危险的失败模式是「为通过而生」。 若提示词里暗示「让测试通过」,LLM 可能反推实现来迎合结果甚至建议改产品代码;提示词必须明确「测试服务需求,产品代码不可动」。
- 无断言与恒真断言是垃圾测试的重灾区。
assertDoesNotThrow、只调不断言的测试让覆盖率虚高却零保护力,流水线里加一条静态检查:测试方法必须包含至少一个具名断言。 - mock 泛滥会让测试退化为「测 mock」。 核心业务协作者尽量用真实实现(内存版、测试容器),只有网络、时钟、随机数等不确定依赖才 mock。
- 覆盖率棘轮必须只升不降且允许例外申请。 生成式代码、纯样板代码可加入豁免清单,否则团队会为了数字生成一堆低价值测试,指标异化就是这么开始的。
- 生成产物的归属与维护要明确。 AI 生成的测试合并后就是团队资产,同样要纳入后续重构与维护,标注生成来源(如类注释)便于追溯,但不设「AI 测试可随意删除」的特权。
收尾
AI 写单测的本质,是把测试的「编写成本」压缩到接近于零,让工程师的精力集中在两件机器做不好的事上:设计值得测的用例、评审测试的有效性。当这套流程跑顺之后,团队的覆盖率曲线会呈现一个特征——存量欠债缓慢上升稀释,新增代码覆盖率稳定在 80% 以上。工具在变,但「测试保护的是重构的勇气」这一判断没变:敢重构的团队,才追得上业务的变化速度。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。