> 关键工具:Claude Code(终端自主 Agent)+ Vitest

> 技术栈阶:高阶——真正的"自建闭环"

> 完整演示:构思 → 创建项目 → 提示词编写 → AI 制作 → 项目检验 → 落地

> 预计耗时:中型模块补到高质量测试约半天

这一篇怎么读

这是导言里 Boris Cherny 所说"自建闭环"的实战版:AI 自己读逻辑、写测试、跑、修,循环到全绿。本篇演示如何让它写出有意义的测试(而非为凑覆盖率作弊),并把测试"落地"为 CI 守门。

阶段一 · 构思:把目标定成"有意义的断言",不是覆盖率

  • 场景:一个裸奔的仓库(核心逻辑没测试),重构和协作如履薄冰。
  • 目标:为核心模块补一套有保护作用的单元测试。
  • 最大风险(构思阶段就要识破):AI 为凑覆盖率会写"运行了代码但什么都没断言"的假测试。覆盖率是极易作弊的指标。
  • 成功标准:每个测试都真的验证了行为;故意改坏源码,测试会变红。

阶段二 · 创建项目:装测试框架、跑通一个样例

  1. 进仓库、启动 Agent
cd <repo> && claude
  1. 装 Vitest 并跑通一个空样例(先确认测试基础设施能跑):
npm i -D vitest
npx vitest run    # 先确认能发现并运行测试
  1. 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 守门人

测试写完不是终点——落地是让它在每次提交时自动跑、挡住坏代码

  1. 加 CI,每次 push / PR 自动跑测试 + 覆盖率:
# .github/workflows/test.yml(关键片段)
- run: npm ci
- run: npm run test:cov     # 测试不过,PR 红灯挡住合并
  1. 设覆盖率"地板"(但记住它是底线不是目标):在 vitest 配置里设 coverage.thresholds,低于阈值 CI 失败——防止以后有人删测试导致质量倒退。
  2. 加 pre-commit 钩子(可选):本地提交前先跑相关测试,把问题挡在推送之前。
  3. 合并后,这套测试就成了项目的"安全网",每次改动都自动验证。

落地的真正意义

测试的价值不在"写出来那一刻",而在它持续地、自动地保护每一次后续改动。接进 CI、变成合并硬门槛,测试才真正"落地"。

30% 复盘:人类到底守住了什么

AI 能自动写测试、自动跑、自动修(70%,这正是"自建闭环"的魅力)。让这些测试"真有保护作用而非刷数字"的 30%,全是人类的质量判断:

30% 工程点人类做了什么不做会怎样
目标设定以"有意义断言"为目标,而非覆盖率AI 优化数字,写一堆假测试
清除假测试揪出无断言/废断言用例90% 覆盖率全是自欺欺人
Mock 正反面强制 mock 异常路径最该测的错误处理根本没测
闭环防呆限制重试、禁止放水断言AI 无限打转或改松断言掩盖问题

核心教训:当你用一个指标考核 AI,AI 就会优化那个指标本身。 别让 AI 替你定义"什么算做好了"——"测试是否真的验证了行为"这个标准,必须牢牢握在人手里。

可复用心法与提示词模板

带得走的三条

  1. 别拿覆盖率当目标:定成"有意义的断言",覆盖率让它自然发生。
  2. Mock 必须覆盖失败路径:外部依赖出错的分支才是测试价值所在。
  3. 用变异测试验收:故意改坏源码看测试会不会红。

通用"让 AI 写测试"提示词骨架

给 <模块> 补单元测试,用 <框架>。
1. 目标是"为关键行为和边界写有意义、有断言的测试",不是覆盖率数字。
2. 每个函数覆盖 正常/边界(空/0/负/超大)/错误;每个测试必须有 expect,
   禁止空测试和永真废断言。
3. 外部依赖 mock 覆盖 成功/异常/空,重点测错误处理路径。
4. 先写 1 个样本给我校准,再批量推进。
5. 自动循环修失败,但同一用例修 N 次不过就停下报告,禁止放宽断言蒙混。