这篇主要解决什么问题
让 AI “给这个函数加测试” 通常生成的是镜像实现的测试——永远通过、抓不到 bug,让你产生覆盖率的错觉。真的 AI 测试生成必须是对抗式的:先列边界 case、再写测试、最后做 mutation 验证测试在该挂时真的挂。这套流程每个非平凡函数耗时 15-30 分钟,常常能在你以为牢靠的代码里挖出 2-4 个真 bug。
这篇适合谁看
用 Claude Code / Cursor / Copilot 或任意 LLM IDE 给现有未测代码补测试的开发者。特别适合工具库、解析函数、计费逻辑、权限检查、任何”算错了也无声又昂贵”的地方。技术 lead 写测试覆盖指南也用得上:这套流程能编码出一份可辩护的”好测试长什么样”。
什么时候适合用
你接手的没测工具函数、有分支的业务逻辑(定价、税务、资格)、刚吸收完 bug 修复需要回归测试的代码、重构前需要安全网的场景。新加入项目时也很有价值——写测试是最快理解函数到底在做什么的方式。
什么时候不建议用
需要真 Chromium、真网络、真实时序的 E2E 浏览器测试——用 Playwright 手写场景。property-based 测试(要随机生成数千输入)——直接用 fast-check 或 Hypothesis。性能基准。UI snapshot——真实信号是视觉 diff 不是逻辑。
开始前准备
- 显式写下函数契约:输入、输出、副作用、错误模式。你三行说不清,AI 也说不清。
- 指定测试框架放进 context。否则你用 Vitest 它给你写 Jest。
- 维护
CLAUDE.md或.cursorrules写约定:断言风格、fixture 路径、命名、什么 mock 什么真。 - 开始前跑一次全套测试确认绿。你得知道哪些挂是新的、哪些是已存在的。
- 识别函数是否有隐藏副作用(文件、网络、时间)。每种都要显式 mock。
具体步骤
- 挑一个函数。让 AI 列 10 个可能让它挂的输入:空、null、超长、负数、unicode、边界值、locale 相关、DST 切换、竞态。显式说:先不写测试。
- 读这份列表。补 2-4 个 AI 漏掉的——通常是领域特定的(昨晚 UTC 0 点过期的 coupon、订阅数为 0 的客户)。
- 让 AI 每个 case 写一个测试,arrange-act-assert,描述性命名。例 prompt:
按上面列表每个 case 写一个测试。用 Vitest。arrange-act-assert。
测试名:should <行为> when <条件>。
不要重构被测函数。除非必要不要加 helper。
- 跑测试。第一天就过的都可疑——打开看断言是否真的覆盖了路径。常见失败:
expect(result).toBeDefined()写成这样而不是expect(result).toEqual(具体值)。 - 每个挂的测试问自己:测试错还是函数错?函数错——恭喜你挖到真 bug,修函数。测试错——修测试。
- 通过后手动跑 mutation:让 AI 给函数植入 3 处合理 mutation(翻一个比较、去掉一个 guard、交换一个参数)。测试都抓到了吗?没抓到就补测试。
- 测试和函数修复分两个 commit 提交,回归历史才清楚。
第一次实操怎么跑
挑你手里最小的纯函数——字符串解析、日期工具、ID 生成器。整套流程包括 mutation 跑一遍。多数开发者会发现:AI 第一遍能列出 60-80% 的边界 case,剩下的真正意外的 bug 来自人工补充。第二次跑只改一个变量:换模型,或者加一份 CLAUDE.md 写约定后重跑。测试质量的 diff 告诉你哪个因素更关键。
完成后检查
- 每个测试都有非平凡断言。不能单独用
toBeTruthy()或toBeDefined()——几乎任何东西都通过。 - 测试名描述行为,不描述实现。“空输入返回 null” 对;“调用 slice(0)” 错。
- 没有任何测试 mock 被测函数本身。AI 有时这么做为了让它过。
- 覆盖率是副产品不是目标。60% 强断言比 95% 弱断言强。
- mutation 测试通过:至少 3 个手工注入的 bug 被测试抓住。
怎么复用这套流程
- 三个 prompt 存成片段:边界 case 列表、按 case 写测试、mutation 检查。换函数只改一行。
- 维护
test-patterns/目录,放领域内好测试的示例。在 prompt 里引用——AI 会模仿形状。 - 维护
ai-test-misses/日志:每次有 bug 上线测试没抓到,把这个 case 加进边界 prompt 模板。 - 测试当内容处理:合并前认真 review diff。AI 写的测试容易被盖章通过然后腐烂。
- 关键函数每季度重跑一次——函数演化、模型演化、边界 case 也演化。
建议的操作流程
挑函数 → AI 列 10 个边界 case(先不写测试)→ 人工补 2-4 个领域 case → AI 每 case 写一个测试 → 跑 → 修挂(测试或函数)→ 通过 3 处手工注入 bug 做 mutation → 测试和修复分开 commit。
容易踩的坑
- 跳过”列边界”直接要测试。测试很浅,镜像实现。
- 不跑测试。AI 偶尔写出意图对但断言错的测试。
- 一条 prompt 同时要”写测试 + 修挂”。AI 常常通过改测试让它过,而不是改函数。
- 把 100% 覆盖当目标。无强断言的覆盖率抓不到 bug。
- 时间 / 网络相关代码忘记 fake timer 和 mock。本地过,CI 挂。
- 测试生成时让 AI 把现有断言搬进 helper。diff 一下子读不懂了。
进阶技巧
- 有分支的函数显式说:每个分支一个测试(每个条件的 true/false、每个 switch 分支、每条错误路径)。
- 异步 / 时间相关代码:让 AI 显式设置 fake timer。在
CLAUDE.md里给一个示例。 - 维护 “test patterns” 文件描述约定:fixture 路径、命名、mock vs 真、允许的外部依赖。AI 同 context 内会遵守。
- 解析函数:边界 case 列表里显式要”非法输入”一节。AI 默认对垃圾输入测试不足。
- 数据库相关代码:让 AI 用内存数据库或事务回滚模式。否则它会建议大范围 mock,证明不了什么。
怎么验收输出
- 边界清单先人工 review 再写测试。
- 本地跑测试通过。
- 至少一个 mutation 测试确认代码挂时测试会挂。
- 没有”不管行为如何都通过”的测试。
- 测试和修复分两个 commit 提交,回归可追溯。
FAQ
- 单测还是集成测用 AI?: 单测最稳。集成测要真环境,AI 经常配错(端口、fixture 路径、清理逻辑)。
- AI 比我写得好吗?: 它生成更多 case 更快,浮出你忘了的边界。最终质量看你的 review。
- 覆盖率工具说 95% 了,为什么还能挖到 bug?: 覆盖率测的是行被执行,不是行为被验证。一个无断言的函数也能 100% 行覆盖。
- 能给无文档的遗留代码生成测试吗?: 能。先让 AI 读函数写出契约,确认契约对再生成测试。
- 每个函数耗时多久?: 非平凡纯函数 15-30 分钟,异步或有状态代码更久。和让 bug 上线在生产环境排查相比,第一次抓到真回归就回本。
- 要自动化 mutation 吗?: Stryker(JS)/ PIT(Java)有现成方案。多数团队 prompt 里手动 mutation 就够,除非测试质量是被量化考核的 KPI。