测试生成 Prompt:写出真正能 catch bug 的测试
让 AI 先列出这段代码的边界条件与失败路径,再针对性生成能真正抓到 bug 的测试用例,附每条用例想验证什么。避免生成一堆只跑 happy path 的凑数测试。
发布 2026-06-21更新 2026-09-23用法
选中要测的函数(或整个模块文件),配合下面的 prompt。
什么时候用:写完一个函数准备提 PR 之前;修完 bug 想加防回归测试时(用文末「进阶」那条变体);接手一段没有测试的遗留代码、需要用测试先锁定行为再动手改时(配合 安全重构 Prompt)。
什么情况下别用:函数还没有稳定的对外契约(需求还在变)。这时生成的测试会把当前的偶然行为固化成契约,需求一变,测试就从资产变成负债。先定契约,再写测试。
怎么粘贴:只贴函数本身 + 它依赖的类型定义 / 常量即可。不要贴整个文件——上下文越长,AI 越容易把不相关的分支也写进来,反而稀释断言密度。如果函数依赖外部服务(数据库、HTTP),在 prompt 里声明「外部依赖请用最小 mock,其余一律真实输入」,否则 AI 会用一个把被测逻辑也一起 mock 掉的假实现,测出 100% 覆盖率、0 个真 bug。
Prompt
请为下面这个函数生成单元测试。要求:
**测试框架**:{vitest / jest / pytest / go test — 替换}
**必须覆盖**:
1. **Happy path**:典型输入 → 期望输出
2. **Edge cases**(每条单独一个 test):
- 空输入(`null` / `undefined` / `[]` / `''`)
- 极小值 / 极大值(如果是数值/字符串长度有意义)
- 边界值(off-by-one 候选)
- 异常输入(类型错误 / 格式错误)
3. **错误处理**:触发函数内部 error path,断言抛出正确的错误类型 + message
**铁律**:
1. **断言要强**。不要只 `expect(result).toBeDefined()`——要 `toEqual` / `toBe` 具体值
2. **每个 test 一个 assertion focus**。不要一个 `it()` 测三件事
3. **测试名要描述行为**:"returns null when input is empty array",**不是** "test 1"
4. **不要 mock 不必要的东西**。能用真实输入就用真实输入
**输出**:
- 完整的 test 文件
- 列出"我故意没测的场景" + 原因(性能测试 / 集成测试 / 不可达分支)
函数:
```
<paste here>
```
为什么有效
- "必须覆盖"列出具体类型,避免 AI 只写 happy path。 AI 的默认倾向是「写出能跑通的那条路」,因为那是它最确信的部分。你不点名,它就不会主动去想
null和 off-by-one。 - "断言要强"是这条 prompt 里收益最高的一句。 AI 默认倾向
toBeDefined()/assertIsNotNone——这类断言几乎捕捉不到任何 bug,却能拉高覆盖率数字。要求toEqual具体值,等于强制它先把期望输出算清楚,而**「算清楚期望输出」这件事本身经常就能发现 bug**。 - "故意没测的场景 + 原因"暴露 AI 的盲点。 这一步的价值不在它列出的清单,而在于它被迫承认自己没测什么。你拿到这份清单后能立刻判断:是合理的取舍(性能测试不该进单测),还是它压根没意识到那条分支存在。
- "不要 mock 不必要的东西"防止假测试。 过度 mock 是 AI 生成测试的头号质量陷阱:
mock掉了数据库、mock掉了时间、最后连被测函数内部的判断分支也mock掉,测试就退化成「验证 mock 是否被按预期调用」——与真实行为无关。
验收:怎么判断它生成的测试是好的
生成完别急着合并,用这四条过一遍。任何一条不过,把这条反馈回给它重生成:
- 变异测试(最有效的一条):把被测函数里的一个判断改坏(
<改成<=、||改成&&、返回值 +1),跑测试——必须有测试失败。没失败说明断言根本没锁住行为,只是跑了一遍代码。 - 删掉 mock 看是否还能跑:能跑说明 mock 是必要的;跑不了但在测业务逻辑,说明 mock 过头了。
- 测试名能不能当文档读:把测试名抽出来单独看,能否还原出这个函数的完整契约?如果看到的全是
test 1/should work,命名不合格。 - 失败信息是否自解释:故意让它失败一次,看报错能不能直接定位到「哪个输入、期望什么、实际得到什么」。需要翻代码才能看懂的失败信息,会在半年后的某次 CI 红灯里浪费你半小时。
进阶
变体 A —— 修完 bug 加防回归测试:
"我刚修了一个 bug:{bug 描述}。请只写能 catch 这个 bug 回归的最小测试集。不要写覆盖整个函数的全套测试——那是别的 PR。"
变体 B —— 给遗留代码补测试(先锁行为后重构):
"下面是我不熟悉的一段遗留代码。请先写一组『描述当前行为』的测试(包括看起来不合理的行为),不要改任何代码。我打算基于这组测试做重构,所以需要的是行为基线,不是理想契约。测完之后,把你认为『这条行为可能是 bug』的地方单独列出来给我判断。"
这个变体是 安全重构 Prompt 的前置步骤——没有测试基线的重构等于裸奔。
变体 C —— 表驱动 / 参数化:如果被测函数是纯函数(同样的输入永远同样的输出),追加一句:
"用表驱动(parameterized)写法:一张
{ 输入, 期望输出, 用例说明 }的表 + 一个循环断言。不要为每条用例写一个独立的it()。"
自动化:让 Claude Code 这类终端 Agent 直接跑「生成 → 执行 → 看失败 → 修」的闭环,比手动复制粘贴高效得多:
claude -p "为 src/utils/date.ts 的 formatRange 生成 vitest 测试,
要求覆盖 null / 跨时区 / 起止时间倒置三种情况,断言必须是具体值。
写完后执行 npx vitest run src/utils/date.test.ts,
如果有失败的断言,自己修到全绿再停。"
加「自己修到全绿再停」这句很关键,但要配合一条纪律:它为了变绿而修改的应该是测试还是被测代码,必须由你来判断。AI 遇到自己写的测试失败时,默认倾向是改测试以适应代码——这在补遗留测试时是对的(锁当前行为),在测新代码时是错的(掩盖 bug)。如果测的是新写的代码,明确追加一句:「测试失败时先怀疑被测代码,不要修改测试来迁就它。」
反例(AI 默认会写的烂版本)
默认输出:it('should work', () => { expect(add(1,2)).toBe(3) })——只测了 happy path,改名、传 null、传边界值全都没覆盖,覆盖率好看但 catch 不到 bug。
加了 prompt 之后:至少覆盖正常路径、边界值(0 / 负数 / 极大值)、异常路径(null / 类型错误)、以及一个「回归用」的历史 bug 用例,并说明每条测试想防住什么。
延伸阅读
- 安全重构 Prompt · 大规模安全重构 Prompt — 重构前先用测试锁定行为
- Bug 定位 Prompt — 修完 bug 用本条加防回归测试
- Claude Code 深度评测 — 最适合自动跑测试、迭代修复的工具