> 关键工具:Claude Code(终端自主 Agent)+ Vitest
> 技术栈阶:高阶——真正的"自建闭环"
> 完整演示:构思 → 创建项目 → 提示词编写 → AI 制作 → 项目检验 → 落地
> 预计耗时:中型模块补到高质量测试约半天
这一篇怎么读
这是导言里 Boris Cherny 所说"自建闭环"的实战版:AI 自己读逻辑、写测试、跑、修,循环到全绿。本篇演示如何让它写出有意义的测试(而非为凑覆盖率作弊),并把测试"落地"为 CI 守门。
阶段一 · 构思:把目标定成"有意义的断言",不是覆盖率
- 场景:一个裸奔的仓库(核心逻辑没测试),重构和协作如履薄冰。
- 目标:为核心模块补一套有保护作用的单元测试。
- 最大风险(构思阶段就要识破):AI 为凑覆盖率会写"运行了代码但什么都没断言"的假测试。覆盖率是极易作弊的指标。
- 成功标准:每个测试都真的验证了行为;故意改坏源码,测试会变红。
阶段二 · 创建项目:装测试框架、跑通一个样例
- 进仓库、启动 Agent:
cd <repo> && claude
- 装 Vitest 并跑通一个空样例(先确认测试基础设施能跑):
npm i -D vitest
npx vitest run # 先确认能发现并运行测试
- 在
package.json里加好脚本,为后面闭环和落地铺路:
"scripts": { "test": "vitest run", "test:cov": "vitest run --coverage" }
为什么先跑通空样例
先确认"测试框架本身能跑",把"环境问题"和"测试质量问题"分开。环境绿了,后面只用关心测试写得好不好。
阶段三 · 具体提示词编写:用措辞堵住作弊
提示词的灵魂:不让 AI 追求覆盖率数字,而要它追求"测试真的验证了行为"。
起手提示词(在仓库根目录启动 claude)
给核心模块补单元测试,用 Vitest。
【目标——注意措辞】不要以"覆盖率达到 X%"为目标。目标是"为每个核心函数的
关键行为和边界写有意义、有断言的测试"。覆盖率是结果不是目标。
【做法】
- 先从 src/core 下不依赖 IO 的纯逻辑函数开始。
- 每个函数覆盖:正常输入、边界(空/0/负/超大)、错误输入。
- 每个测试必须有明确 expect 断言,禁止只调用不断言的"空测试"。
- 写完一批就跑 vitest,把失败的修到通过再继续。
先挑 1 个核心函数把测试写出来给我看风格,我确认后再批量推进。"先挑 1 个给我看风格"——在批量产出前用样本校准质量基线。
阶段四 · AI 制作:看产出与第一处不足
AI 为 calculateDiscount 写了 5 个测试,风格不错,但漏了最该测的边界(折扣为负、原价为 0)。这正是要校准的地方:
// AI 初版只测了"正常情况",人补充边界后:
it('折扣为负数时应抛错', () => {
expect(() => calculateDiscount(100, -0.2)).toThrow();
});
it('原价为 0 时返回 0', () => {
expect(calculateDiscount(0, 0.5)).toBe(0);
});
校准提示词
风格可以,但边界不够。这个函数最易错的是:折扣>100%、折扣为负、
原价为0或负、金额浮点精度。补上这些边界。以后每个函数都要这样穷举边界。校准好基线,再让它批量推进。第一个样本花的时间,省下后面几十个函数的返工。
阶段五 · 项目检验(上):揪出作弊、补全 Mock
5.1 揪出"凑覆盖率"的假测试
批量产出后审计,发现作弊:
it('handles user data', () => {
processUser(mockUser); // 调用了,但没有任何 expect —— 纯刷覆盖率
});
清除假测试提示词
扫描所有测试,找出"调用了函数但没有 expect 断言"的空测试列清单。
这些是为凑覆盖率的无效测试,要么补有意义的断言、要么删。
禁止用 expect(true).toBe(true) 这类废断言充数。指标陷阱
覆盖率 90% ≠ 质量好。用"覆盖率"考核 AI,它就优化这个数字本身。对策:把考核标准换成"每个测试是否有意义的断言",并由人抽查。
5.2 核心难点:外部依赖的 Mock 要覆盖失败路径
依赖数据库/API 的函数,AI 初版把调用 mock 成永远成功,于是"出错时怎么处理"这条最重要的路径根本没测到:
Mock 策略提示词
数据库相关测试,Mock 要覆盖正反两面,不能只 mock 成功:
- mock 正常数据(happy path)。
- mock 抛连接错误/超时 → 验证函数是否正确处理异常、回滚、报错。
- mock 空结果/字段缺失 → 验证兜底逻辑。
用 vi.mock,别让测试依赖真实数据库。Mock 的价值恰恰在于模拟"外部出问题"——这正是 AI 最爱偷懒、只 mock 成功的地方。
5.3 闭环防呆
闭环防呆提示词
实现自动循环:写完就跑 vitest,失败的自动分析并修复再跑。
但限制:同一测试连续修 3 次还不过就停下报告,不要无限重试,
也不要为了让它过而把断言改宽松(那是掩盖问题)。"不要靠放宽断言蒙混"堵住了最隐蔽的作弊:测试失败时该修代码或确认预期,而不是把测试改得更容易过。
阶段五 · 项目检验(下):用变异测试验收
验收狠招——变异测试
想知道测试是不是真有用?故意把源码改坏一行(如 > 改 >=),跑测试。如果没有任何测试变红,说明那段测试是摆设。这比看覆盖率数字可靠得多。
| 检验项 | 操作 | 期望 |
|---|---|---|
| 断言审计 | 抽查测试 | 都有真实 expect,无空测试/废断言 |
| 变异测试 | 故意改坏一处源码逻辑 | 有测试因此变红 |
| 异常路径 | 查 DB/API 出错分支 | 真的被测到 |
| 闭环防呆 | 给一个改不动的用例 | 停下报告,不死循环、不放水断言 |
阶段六 · 落地:把测试变成 CI 守门人
测试写完不是终点——落地是让它在每次提交时自动跑、挡住坏代码。
- 加 CI,每次 push / PR 自动跑测试 + 覆盖率:
# .github/workflows/test.yml(关键片段)
- run: npm ci
- run: npm run test:cov # 测试不过,PR 红灯挡住合并
- 设覆盖率"地板"(但记住它是底线不是目标):在 vitest 配置里设
coverage.thresholds,低于阈值 CI 失败——防止以后有人删测试导致质量倒退。 - 加 pre-commit 钩子(可选):本地提交前先跑相关测试,把问题挡在推送之前。
- 合并后,这套测试就成了项目的"安全网",每次改动都自动验证。
落地的真正意义
测试的价值不在"写出来那一刻",而在它持续地、自动地保护每一次后续改动。接进 CI、变成合并硬门槛,测试才真正"落地"。
30% 复盘:人类到底守住了什么
AI 能自动写测试、自动跑、自动修(70%,这正是"自建闭环"的魅力)。让这些测试"真有保护作用而非刷数字"的 30%,全是人类的质量判断:
| 30% 工程点 | 人类做了什么 | 不做会怎样 |
|---|---|---|
| 目标设定 | 以"有意义断言"为目标,而非覆盖率 | AI 优化数字,写一堆假测试 |
| 清除假测试 | 揪出无断言/废断言用例 | 90% 覆盖率全是自欺欺人 |
| Mock 正反面 | 强制 mock 异常路径 | 最该测的错误处理根本没测 |
| 闭环防呆 | 限制重试、禁止放水断言 | AI 无限打转或改松断言掩盖问题 |
核心教训:当你用一个指标考核 AI,AI 就会优化那个指标本身。 别让 AI 替你定义"什么算做好了"——"测试是否真的验证了行为"这个标准,必须牢牢握在人手里。
可复用心法与提示词模板
带得走的三条
- 别拿覆盖率当目标:定成"有意义的断言",覆盖率让它自然发生。
- Mock 必须覆盖失败路径:外部依赖出错的分支才是测试价值所在。
- 用变异测试验收:故意改坏源码看测试会不会红。
通用"让 AI 写测试"提示词骨架:
给 <模块> 补单元测试,用 <框架>。
1. 目标是"为关键行为和边界写有意义、有断言的测试",不是覆盖率数字。
2. 每个函数覆盖 正常/边界(空/0/负/超大)/错误;每个测试必须有 expect,
禁止空测试和永真废断言。
3. 外部依赖 mock 覆盖 成功/异常/空,重点测错误处理路径。
4. 先写 1 个样本给我校准,再批量推进。
5. 自动循环修失败,但同一用例修 N 次不过就停下报告,禁止放宽断言蒙混。