说句实话,在没有 AI 工具之前,我对写测试这件事的态度是「知道应该写,但总是找借口不写」。理由总是很充分的:功能都还没写完呢,先赶进度;这段逻辑很简单,不会出问题的;测试写起来太烦了,改一下实现就得改一堆测试……
Claude Code 彻底改变了这个局面。它写测试的速度和写业务代码一样快,你不再需要在「赶进度」和「写测试」之间做痛苦的取舍。测试的边际成本降到了近乎为零,它真的变成了每个步骤的标配。
现在我的态度完全反过来了:不带测试的代码让我缺乏安全感。有了 AI 写测试之后,你才意识到以前裸奔开发是多么大胆的行为。
根据场景不同,我在实际开发中交替使用三种工作流。没有哪种绝对更好,关键是选择适合当前情况的那个。
这是最直觉的方式。让 Claude Code 先写好功能代码,确认逻辑大致正确之后,再让它补上测试。适合那些你已经很清楚要实现什么、逻辑相对明确的场景。
$ "为 createOrder 函数写单元测试:
- 正常创建 / 库存不足 / 金额含折扣 / 无效 ID
使用 vitest,mock 数据库调用"
这里有个技巧:列出你想覆盖的测试场景,而不是只说「写个测试」。如果你不指定场景,Claude Code 倾向于只覆盖 happy path,那些真正容易出 bug 的边界情况反而会被忽略。
我一般会在 prompt 里列出三到五个关键场景,包括至少一个正常流程和两三个异常情况。边界场景是测试真正发挥价值的地方——正常流程出问题你很快就能发现,但边界情况的 bug 可能潜伏几个月才被触发。
这是我越来越偏爱的方式,虽然一开始觉得有点反直觉。流程是先让 Claude Code 写测试(此时实现还不存在,测试应该全部失败),然后再让它写实现代码让测试通过。
# 步骤 1:先写测试(应该全部失败)
$ "createOrder 函数还不存在。先写测试,覆盖以下场景:
正常创建、库存不足抛异常、折扣计算精度到分"
# 步骤 2:写实现让测试通过
$ "现在实现 createOrder,让所有测试通过"
TDD 和 Claude Code 配合有一个很妙的效果:测试实际上充当了一份精确的「验收标准」。当你先写测试,你就是在用代码语言定义「这个函数的正确行为是什么」。然后 Claude Code 拿着这个明确的目标去写实现,产出质量会比没有测试目标时高很多。
有一次我在做一个价格计算模块,先用 TDD 方式让 Claude Code 写了一组测试,涵盖了各种折扣叠加的规则。然后让它写实现——一次就全部通过了。如果是先写实现,我几乎可以肯定折扣叠加的逻辑至少要改两轮。
这个工作流专门用于改造已有代码。流程是三步:先为现有代码补上测试,确认测试全部通过,然后在测试保护下放心大胆地重构。
这个场景在实际工作中特别常见。你接手一段没有测试的老代码,想重构但又怕改出问题。以前你可能只能硬着头皮改,然后手动测试一遍祈祷没 bug。现在有了 Claude Code,补测试的成本很低。
我的经验是,让 Claude Code 补测试的时候,要把现有代码和它的调用方一起贴给它看。这样它不仅知道函数本身的行为,还能理解这个函数在实际场景中是怎么被使用的,写出来的测试会更贴近真实情况。
别忘了,AI 写的测试和它写的业务代码一样,需要你过一遍。我见过几种典型的测试质量问题:
测试看起来很多,但其实都在测同一条路径的不同写法,真正的边界情况一个都没覆盖。还有一种情况是测试和实现耦合太紧——断言写的是实现细节而不是行为,这样稍微重构一下实现,测试就全挂了,但实际上功能根本没变。
最隐蔽的是「永远通过」的测试。Mock 写得不对,导致不管实现怎么改测试都是绿的。这种测试不仅没有价值,还会给你虚假的安全感。
审查测试时,我会特别关注:断言是否在验证真正重要的行为?Mock 是否正确模拟了依赖的行为?是否有明显的场景被遗漏?
一个简单的验证方法:故意把实现里的一行关键逻辑改错,跑一下测试。如果测试还是全绿的,说明测试没有真正保护到那段逻辑。这个技巧叫「变异测试」的手动版,虽然笨但很有效。
每周更新 Claude Code 实战技巧、工具对比、行业动态。回复「模板」获取 CLAUDE.md 模板合集。