AI 写单测的工作流——能信得过的测试

AI 写的测试经常"通过但啥也没测"。下面这套流程给你真覆盖率。

这篇主要解决什么问题

让 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。

具体步骤

  1. 挑一个函数。让 AI 列 10 个可能让它挂的输入:空、null、超长、负数、unicode、边界值、locale 相关、DST 切换、竞态。显式说:先不写测试。
  2. 读这份列表。补 2-4 个 AI 漏掉的——通常是领域特定的(昨晚 UTC 0 点过期的 coupon、订阅数为 0 的客户)。
  3. 让 AI 每个 case 写一个测试,arrange-act-assert,描述性命名。例 prompt:
按上面列表每个 case 写一个测试。用 Vitest。arrange-act-assert。
测试名:should <行为> when <条件>。
不要重构被测函数。除非必要不要加 helper。
  1. 跑测试。第一天就过的都可疑——打开看断言是否真的覆盖了路径。常见失败:expect(result).toBeDefined() 写成这样而不是 expect(result).toEqual(具体值)
  2. 每个挂的测试问自己:测试错还是函数错?函数错——恭喜你挖到真 bug,修函数。测试错——修测试。
  3. 通过后手动跑 mutation:让 AI 给函数植入 3 处合理 mutation(翻一个比较、去掉一个 guard、交换一个参数)。测试都抓到了吗?没抓到就补测试。
  4. 测试和函数修复分两个 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。

相关阅读

标签: #AI 编程 #教程 #工作流