
前阵子我接手一个内部工具项目整整四天没写一行业务代码先补了一堆测试。同事一开始以为我在摸鱼直到第五天开始我把写好的测试文件丢给Claude Code让它顺着这些测试把实现补全每天稳定产出八十到一百行有效代码跑一次测试绿了就是绿了不用猜。这件事让我彻底改变了看法TDD测试优先开发在AI时代并没有过时它反而是让AI编码从“能跑”走向“靠谱”的最关键手段。如果你也在用Claude Code这类AI编程助手做开发一定遇到过这种情况——让AI直接写一个功能它三分钟写完跑起来却发现边界条件全是坑反过来你把验收条件写成测试再交给它同样的模型产出的代码质量立刻上了一个台阶。这篇文章我想聊聊怎么把TDD测试优先这套老方法论和Claude Code的工作方式结合起来形成一套能落地、可重复的AI开发流程。适合正在用Claude Code写业务代码的开发者也适合那些想让AI辅助开发回归可控状态的团队。1. 为什么AI开发反而更需要TDD1.1 AI代码助手的最大问题不是生成而是验证AI编程工具发展到现在生成代码的能力已经强到“只要需求说清楚代码量根本不是事”。于是真正的痛点就变成了它生成的代码是不是真的满足需求模型很擅长把一段代码补得看起来合理但它不会在每行代码后面都问自己一句“这个边界处理对吗”。实际开发里最常见的翻车点是主流程跑通了空值、负数、超长字符串、并发、异常路径这些AI默认不会主动考虑因为它训练时学到的“最常见写法”里往往就没有这些防御逻辑。你可以把AI当成一个特别聪明但有点自信过头的实习生。它速度快、理解力强但如果没有一套明确的验收机制它就会用自己的想象去填补需求里的空白而它想象出来的东西往往和业务真实要求有偏差。我见过太多“AI写完了但一上线就被测试环境打回来”的场景根因基本都一样人类没把验收标准表达清楚。1.2 测试优先本质上是一份“AI能读懂的验收标准”测试优先的价值恰恰在于它把模糊的自然语言需求变成一组合格的、可执行的形式化断言。AI不需要去猜“这里到底允不允许负数”测试文件里白纸黑字写着“传负数必须抛异常”。这种约束不是限制AI反而是帮AI减少选择让它把算力集中在真正的实现逻辑上。打个比方你让一个外包师傅砌墙告诉他“砌一堵墙”他可能给你砌成任何样子但你给他一张施工图墙高多少宽多少、门洞在哪、用多少标号的水泥他做出来的东西就是你要的东西。测试文件就是那张施工图。尤其在Claude Code这种能直接读懂项目文件、能看到测试代码的AI工具面前你给它测试它就能精准干活。还有一个容易被忽略的好处可回归。AI改完代码后测试补丁一跑通过就是通过不用靠人对diff逐行review。这在迭代速度极快的AI辅助开发节奏里是最值钱的护城河。1.3 谁负责写测试谁负责写实现在传统TDD里开发和测试通常由同一个人完成顺序是先测试后实现。但在AI开发模式下这个分工可以拆得更开测试由人写或者由人严格审查实现交给AI去补全。因为测试是在表达“业务到底要什么”这是机器理解不了、也最容易跑偏的部分而实现是“怎么把需求翻译成代码”这正是模型的强项。我现在的习惯是任何功能第一件事不是打开Claude Code说“帮我写个xx功能”而是先花十分钟把测试用例梳理好。这个过程看似慢了但恰恰省掉了后面至少一小时的扯皮和返工。让AI直接写实现它写出来的东西离测试约束的目标很近很少会出现“方向性错误”。如果你希望团队里每一个人都对AI产出有掌控感记住这句话AI负责速度测试负责方向。2. Claude Code安装与基础配置2.1 Claude Code到底是什么Claude Code是Anthropic推出的命令行AI编程工具和那些只能在聊天框里粘代码的网页版不一样它直接跑在终端里能读取整个项目目录、修改文件、执行命令、调用测试甚至可以用并行子任务去处理多个独立问题。也就是说它会真的“上手干活”而不是只给你建议。这种工具形态和TDD天然契合。因为TDD是一个频繁“跑测试—看结果—改代码”的循环Claude Code正好能把整个过程串起来你说“跑一遍测试”它就真的去跑你说“根据失败信息改”它就真的去改。如果只靠网页聊天框你要不停复制粘贴代码和报错效率至少打五折。2.2 安装步骤从空目录到能跑起来Claude Code的安装门槛不高前提是你的机器上有Node.js建议版本18以上太老的版本容易遇到依赖问题和npm。装的时候直接执行npm install -g anthropic-ai/claude-code安装完成后在项目根目录运行claude首次启动会要求你完成登录授权一般是用Anthropic账号登录或者配置API Key。装完可以先用claude --version确认版本没问题。Windows上建议用Windows Terminal来跑体验最好如果是在WSL里用Ubuntu那基本和Linux一致坑很少。Ubuntu环境装Node后直接全局安装就行不需要额外改动。还要注意一点Claude Code本质上是连接远程大模型服务所以网络环境要能正常访问对应的API端点否则连不上、超时这些情况都会冒出来。如果你不是使用Anthropic官方API而是通过模型网关接入其它大模型社区里常聊到DeepSeek这类核心就是设置好API地址和密钥对应的环境变量CLAUDE.md一样照常生效。具体配置方式更新比较快建议直接查官方文档或看社区最新教程。2.3 VSCode配置与CLAUDE.md项目规范在VSCode里用Claude Code不需要装额外插件直接在“终端”面板里运行claude即可。但有一个文件非常关键叫CLAUDE.md放在项目根目录相当于这个项目的“操作手册”。我通常会写清楚这四类信息项目用的语言、框架、包管理工具测试命令是什么比如npm test -- --runInBand目录结构约定编码规范比如“必须使用严格模式”“不要引入额外依赖”CLAUDE.md写得好Claude Code在每次回答前都会自动读一遍这样它生成代码的风格会被约束到项目习惯上。这也是让TDD流程稳定的基础配置如果你连测试命令都没写进去AI跑测试的时候可能会用错工具或者跑错了目录白白浪费时间。3. 测试优先的AI开发完整工作流3.1 红绿重构循环在Claude Code里怎么落地传统TDD的循环是红灯写失败测试—绿灯让测试通过—重构清理代码。在Claude Code里这个循环可以这样映射人写测试或者让Claude Code先生成测试草稿再由人逐条review。跑一遍确认红灯测试失败因为实现还不存在或不对。把测试文件和分析指令交给Claude Code让它实现。跑测试确认全绿。让Claude Code做重构然后重新跑测试确认仍是绿色。这里我特别强调第2步红灯必须真红。如果测试在还没有实现的时候就通过了说明这个测试写得太弱等于没写。很多AI生成的测试就有这个毛病断言过于宽松任何实现都能过这样的测试放进项目里只是图个心理安慰。宁可不要也不能留。3.2 第一步把需求翻译成测试红把需求写成测试是整个过程里最需要人的判断力的一步。举个例子你接了一个需求“实现一个折扣计算函数”。自然语言需求里没有告诉你这些细节折扣怎么传是0.8还是80能不能传100%负数和大于1的折扣怎么办结果要不要四舍五入这些问题没人规定AI就会自己猜。但如果你用测试把这些问号全部钉死AI就没得猜了。下面是我用Jest写的一个典型测试文件// src/__tests__/priceCalculator.test.js const { calculateDiscount } require(../src/priceCalculator); describe(calculateDiscount, () { test(正常折扣原价100打8折返回80, () { expect(calculateDiscount(100, 0.8)).toBe(80); }); test(边界折扣0%折扣返回原价, () { expect(calculateDiscount(100, 0)).toBe(100); }); test(边界折扣100%折扣返回0, () { expect(calculateDiscount(100, 1)).toBe(0); }); test(非法参数折扣大于1时抛出异常, () { expect(() calculateDiscount(100, 1.1)).toThrow(折扣必须在0到1之间); }); test(非法参数价格为负数时抛出异常, () { expect(() calculateDiscount(-10, 0.5)).toThrow(价格不能为负); }); test(处理浮点数原价88.5打7折保留两位小数, () { expect(calculateDiscount(88.5, 0.7)).toBe(61.95); }); });写完之后先跑一次npx jest src/__tests__/priceCalculator.test.js你会看到一堆红色失败因为priceCalculator.js还不存在。这个红色是好事它说明测试确实在约束行为。3.3 第二步让AI看着测试去实现绿现在打开Claude Code把测试文件作为上下文丢给它指令可以这样写“项目里有一个测试文件src/__tests__/priceCalculator.test.js请实现对应的src/priceCalculator.js模块要求不要修改测试文件只实现代码确保所有测试通过。注意边界条件函数签名严格匹配测试里的require。”这个指令的关键点在于明确告诉AI“测试就是需求”。它不需要纠结业务怎么定义只需要让断言通过。这大大降低了AI理解需求的难度生成的代码泛化性也会好很多。以刚才那个测试为例AI给出的实现大概是这样的// src/priceCalculator.js function calculateDiscount(price, discount) { if (price 0) { throw new Error(价格不能为负); } if (discount 0 || discount 1) { throw new Error(折扣必须在0到1之间); } return Math.round(price * discount * 100) / 100; } module.exports { calculateDiscount };跑一遍测试全绿之后第二步就算完成了。这里有个小技巧如果一次没跑绿把终端里的失败信息原样贴回给Claude Code让它看具体断言它会自己修正。不要笼统地说“还有问题”要把错误堆栈给它就像你在指导一个刚入职的同事。3.4 第三步用AI做重构但要小心“顺手改坏”全绿之后的重构阶段AI特别好用。你只需要说“在不改变行为的前提下把这个函数拆成两个更小的函数保持测试通过”它就会自己动手而你只需要跑一遍测试确认还是绿的。但这里有几个坑我踩过好几次。第一AI重构时偶尔会“顺手”优化掉看似无用但其实有意义的防御逻辑比如把边界判断合并掉结果测试挂了。第二它有时候会“顺手”去改测试文件把断言改得宽松让测试重新变绿。所以我在重构阶段给它的约束是禁止修改测试文件只能修改实现每次修改后告诉我哪个测试命令可以验证。如果它改坏了git diff查一下改动用git checkout .还原再让它换个思路重来。测试在这里就是安全网没有这张网AI重构就是一场赌博。4. 三个高频场景的实操案例4.1 业务算法价格计算器上面已经展示了价格计算器这里补充一个实际业务里更常见的需求阶梯折扣。比如订单满100打9折满500打8折不满100不打折。这种规则用自然语言描述很容易有歧义但写成测试就一目了然// src/__tests__/orderDiscount.test.js const { calcOrderDiscount } require(../src/orderDiscount); test(订单金额小于100不打折, () { expect(calcOrderDiscount(99)).toBe(99); }); test(订单金额满100打9折, () { expect(calcOrderDiscount(100)).toBe(90); }); test(订单金额满500打8折, () { expect(calcOrderDiscount(500)).toBe(400); }); test(金额为0返回0, () { expect(calcOrderDiscount(0)).toBe(0); }); test(负金额抛出异常, () { expect(() calcOrderDiscount(-1)).toThrow(金额不能为负); });这种测试把“边界到底是多少”直接写死AI实现的时候就不会再纠结“满100到底折不折”这种问题。实测下来这类规则型代码出错率极低因为输入输出被钉得很死。4.2 数据校验表单验证逻辑另一个非常常见、也非常适合先写测试的场景是数据校验。比如用户注册表单邮箱格式、密码长度、必填项这些规则看起来简单组合起来之后边界极多。如果让AI直接写校验函数它经常会漏掉“邮箱为空”或者“密码刚好8位”这种边界。先写测试就完全不一样// src/__tests__/validator.test.js const { validateUserInput } require(../src/validator); test(合法输入通过校验, () { const result validateUserInput({ email: testexample.com, password: abc12345, }); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); test(非法邮箱返回对应错误信息, () { const result validateUserInput({ email: not-an-email, password: abc12345, }); expect(result.valid).toBe(false); expect(result.errors).toContain(邮箱格式不正确); }); test(密码太短返回对应错误信息, () { const result validateUserInput({ email: testexample.com, password: abc123, }); expect(result.valid).toBe(false); expect(result.errors).toContain(密码至少8位); }); test(邮箱为空提示必填, () { const result validateUserInput({ email: , password: abc12345, }); expect(result.valid).toBe(false); expect(result.errors).toContain(邮箱不能为空); });把这个测试丢给Claude Code它实现出来的校验函数不仅会覆盖主流程还会把错误信息收集成数组而不是遇到第一个错误就return。这就是测试约束的效果每个用例都对应一条真实业务规则AI的“自由发挥”空间被压缩到了合理范围。4.3 外部依赖Mock掉API调用第三个高频场景是接口集成。很多业务函数要请求外部API比如根据当前汇率做货币转换。如果让AI实现它通常会真的发请求但测试环境不应该依赖网络。这时候就需要在测试里Mock掉请求函数把外部依赖隔离掉。// src/__tests__/currency.test.js const { convertCurrency } require(../src/currency); test(汇率转换USD到CNY使用mock汇率, async () { const getRate jest.fn().mockResolvedValue(7.2); const result await convertCurrency(100, USD, CNY, { getRate }); expect(result).toBe(720); }); test(汇率转换金额为0结果为0, async () { const getRate jest.fn().mockResolvedValue(7.2); const result await convertCurrency(0, USD, CNY, { getRate }); expect(result).toBe(0); });注意这里我把getRate设计成依赖注入的形式而不是在函数内部直接require一个http库。这样测起来特别干净。让AI实现时它也会倾向于写出可测试的代码因为它看到测试里这么调用了自然而然会把依赖参数化。这也是测试优先引导架构设计的一个例子——好的测试设计会倒逼出好的代码结构。5. 常见问题与排查经验5.1 AI生成的测试为什么会“假绿”我在分享这个工作流时被问得最多的就是“让AI自己写测试行不行”行但一定要人审。AI写的测试太容易出现“假绿”了。常见套路有三种第一种断言过弱。比如测试一个排序函数只断言返回结果包含所有元素却不断言顺序。数组里元素全在但顺序完全错测试照样通过。第二种测试里直接复制了实现逻辑。AI有时候会把if (price * discount 100)这种内部逻辑原封不动写进断言里等于用同一套算法验证自己永远测不出错。第三种Mock掉了不该Mock的东西。比如本来要测试缓存逻辑结果AI把缓存函数本身mock了整个测试看似跑通实际上测了个寂寞。所以我的原则是AI生成的测试说得再好听也必须由人逐条看断言尤其是边界用例和异常用例。测试不是给机器看的仪式是给项目定规矩的合同。5.2 测试覆盖够了但AI还是跑偏怎么办有时候测试写得挺全但AI实现的代码要么多出一堆不需要的功能要么把逻辑绕得很复杂这时先别急着怪模型可能是你的指令给了它过度发挥的空间。我常用的纠偏指令是这样“请严格遵循测试文件的意图用最小改动完成实现。不要添加测试之外的新功能不要过度设计不要修改现有API签名。”这句话很有用。另外如果你发现AI频繁跑偏大概率是CLAUDE.md里缺少约束。把“本项目优先采用简洁实现不引入额外抽象”这样一句话写进去比每次在对话里强调一百遍都管用。还有一个更笨但很可靠的办法把测试文件拆小一次只丢给它三个用例绿了再丢下一批。对复杂模块小步快跑比一次性塞一个大文件成功率高很多。5.3 一套可以直接抄的提问模板总结这套TDD流程我把我最常用的提问模板贴在下面直接复制就能用我采用TDD流程。请按以下顺序执行 1. 先阅读测试文件【文件路径】理解被测函数的行为约束。 2. 用最小改动实现功能不要修改测试文件。 3. 运行测试命令【测试命令】如失败则根据错误信息修复直到全部通过。 4. 全部通过后用简短语言说明你的实现思路特别是边界条件的处理方式。这个模板的核心是“顺序”。先读测试再写实现最后验证。只要顺序不乱AI的表现就会很稳定它不会跳过测试文件也不会跑到一半自己发挥。6. 几点个人心得与后续玩法6.1 让AI写测试是辅助不是甩锅我不建议一上来就把整个模块的测试丢给AI写尤其是你还没想清楚业务规则的时候。AI生成的测试可以作为“草稿”给你一些边界场景的灵感但最终落到文件里的每一条用例都得能回答一个问题这条用例对应哪条业务规则如果答不上来就是无效测试。6.2 测试优先不只是一套流程也是一种协作方式当团队里人使用AI编程时测试文件其实成了人和AI之间最清晰的“接口契约”。人把契约定义好AI照着执行大家不用反复猜对方什么意思。从我自己的项目经验看这套玩法不仅让代码质量上去了还让我重新找回了那种“对代码有掌控感”的踏实。如果你手里正有一个Claude Code项目不妨从下一个功能开始先写测试再让它动手。试上一周你会回来感谢TDD的。