
AI 时代的 TDD:先让模型撞上红灯
AI 编程里为什么还需要 TDD。重点不是形式上的“先写测试”,而是先制造一个可验证的失败,再让实现从红变绿。
先说结论

AI 写代码以后,TDD 不是过时了,而是换了一个位置。
过去我们说 TDD,常常是在说程序员的自律:先写测试,再写实现,小步重构。到了 AI 编程里,它更像一套刹车系统。因为模型最擅长的事,正好也是最危险的事:它能很快写出一大段看起来完整的代码。
你让它实现一个功能,它可能十几秒就给你:
- 一个实现文件
- 一组测试
- 一段解释
- 一句“已完成”
问题是,“看起来完整”不是工程意义上的完成。工程意义上的完成,至少要回答:
这个行为有没有被一个明确的失败测试定义过?
这个失败有没有因为实现而变绿?
变绿以后,我们有没有在不改行为的前提下整理代码?
这就是 AI 时代重新谈 TDD 的原因。
它不是为了让流程显得高级,而是为了把“相信模型”换成“相信反馈”。
一、常见误解:TDD 不是“先写测试”

很多人讨厌 TDD,是因为他们理解成了一个仪式:
先写测试。
再写代码。
最后跑一下。这当然无聊,而且很容易变成形式主义。
真正有用的 TDD,不是“测试文件出现得比较早”,而是“失败出现得足够早”。
关键不是测试,而是红
TDD 的第一步叫 RED,不叫 TEST。
RED 的意思是:先写一个测试,让系统明确失败。这个失败必须满足三件事:
- 它确实失败。
- 它因为目标行为不存在而失败。
- 它失败的方式和你的预期一致。
如果没有先看到红,后面的绿就没有意义。
比如你要实现 slugify("Hello World") -> "hello-world"。一个有价值的 RED 不是“我写了个测试文件”,而是:
Test: slugify.test.ts
Command: npm test -- slugify
Failure: slugify is not defined
Reason: 目标函数还不存在,符合预期这时测试才变成了规格。它告诉你:下一步实现只需要让这一条行为成立。
先绿再补测试,通常是在补故事
AI 很容易走另一条路:先写实现,再补测试。
这在体验上很顺。你看到代码已经跑了,测试也有了,心里会觉得“差不多”。但它有一个致命问题:测试很可能只是对当前实现的追认。
它不是在问“需求应该是什么”,而是在问“当前代码怎么写才容易通过”。
这就是为什么 AI 写测试经常会出现这些味道:
- 断言太贴合当前实现
- mock 太多,真实边界没测到
- 只测 happy path
- 为了让现有代码通过,把断言写得很宽
- 没有一个测试能证明旧代码原本是错的
TDD 要反过来:先让需求变成失败,再让代码追上需求。
二、为什么 AI 时代更需要 TDD
AI 编程的核心矛盾,不是“代码写得慢”,而是“反馈来得晚”。
没有 TDD 时,你通常是这样工作:
描述需求 -> AI 写一堆代码 -> 人肉看 diff -> 跑一下 -> 发现问题 -> 回头修问题会堆到最后。等你发现它错了,可能已经有三类东西混在一起:
- 需求理解错了
- 实现路径错了
- 重构把旧行为改坏了
TDD 的作用,是把这个长链条切短。
它给模型一个可判定目标
“写得优雅一点”不是目标。
“实现用户登录态过期后自动跳回登录页”也还不够具体。
更好的目标是:
当 access token 过期时:
1. 请求返回 401。
2. 客户端清理本地 session。
3. 用户被重定向到 /login。
4. 原始目标地址被保存在 redirect 参数里。再往前一步,把其中一条变成失败测试:
given expired session
when user opens /settings
then app redirects to /login?redirect=/settings这时 AI 不再是在猜“登录态过期应该怎么处理”,而是在完成一个明确行为。
它把大任务拆成小闭环
AI 最容易失控的地方,是一口气做完。
一次性让它实现登录、权限、刷新 token、错误提示、路由跳转,最后你会得到一个很大的 diff。它也许能跑,但 review 成本很高。你要同时判断业务、状态、路由、边界、测试和重构。
TDD 的节奏更像这样:
一个行为 -> 一个失败测试 -> 最小实现 -> 变绿 -> 再下一个行为每次只推进一小段。小到你能看懂,小到 AI 不容易编故事,小到失败时能快速定位。
它限制模型“顺手发挥”
AI 的一个常见问题是热心过度。
你让它修一个边界 bug,它顺手抽了 helper;你让它加一个测试,它顺手改了实现;你让它重构,它顺手改了行为。
TDD 用阶段把这些动作分开:
| 阶段 | 可以做什么 | 不该做什么 |
|---|---|---|
| RED | 写一个失败测试 | 写生产实现 |
| GREEN | 写最小实现 | 改测试凑绿 |
| REFACTOR | 整理结构 | 引入新行为 |
这张表比“请谨慎一点”有用。它让模型知道现在处在哪个阶段,也让人类更容易发现越界。
三、红绿重构:三道门,不是三句口号

“Red, Green, Refactor” 很容易被说成口号。真正用起来,它应该像三道门。每过一道门,都要留下证据。
第一门:RED,证明需求还没被满足
RED 阶段最重要的问题是:
这个测试如果失败,是否能证明我们还缺一个目标行为?
一个坏 RED:
expect(true).toBe(true)一个也不太好的 RED:
expect(formatTitle("Hello World")).toContain("hello")它太宽了。很多错误实现也能通过。
更好的 RED:
expect(slugify("Hello World")).toBe("hello-world")这条测试很小,但它清楚。它指定了输入、输出和行为。
第二门:GREEN,只让当前测试通过
GREEN 阶段不是写最终架构。
它的任务只有一个:用最少代码让当前失败测试通过。
这句话听起来反直觉。很多人会担心“最少实现”会不会太丑。会,有时候会丑。但它的价值在于保持设计压力。
如果第一条测试是:
expect(slugify("Hello World")).toBe("hello-world")一个可以接受的 GREEN 可能只是:
export function slugify(input: string) {
return input.toLowerCase().replace(/\s+/g, "-");
}你不需要立刻支持中文、重音符号、连续标点、emoji、SEO 特例。那些应该由后面的测试推动。
第三门:REFACTOR,只改结构,不改行为
REFACTOR 阶段最容易被 AI 搞混。
它会把“整理代码”理解成“顺便增强一下”。这不行。重构的定义很窄:外部行为不变,内部结构变好。
好的重构像这样:
- 改一个更准确的变量名
- 抽出重复表达式
- 拆掉过深的条件分支
- 移动函数位置,让模块职责更清楚
坏的重构像这样:
- 顺手支持了新输入
- 顺手改了错误提示
- 顺手换了依赖
- 顺手改了测试断言
判断标准很简单:
如果这个提交只叫
refactor:,测试前后应该一样绿,用户行为也应该一样。
四、好测试的味道
TDD 不是测试越多越好。AI 也很擅长生成一堆没什么价值的测试。
更重要的是测试的味道。
好测试像规格
好测试读起来应该像一句业务规格:
当用户没有权限时,保存按钮不可点击。
当标题为空时,表单显示错误信息。
当重复提交同一个请求时,只创建一条记录。它关心的是外部行为,而不是内部怎么做。
坏测试则像实现笔记:
应该调用 validateInput 三次。
应该读取 state.user.flags。
应该触发 handleClick 内部函数。实现细节被绑住以后,重构就会很痛。你只是改了内部结构,测试却大面积失败。这样的测试不是保护代码,而是在冻结代码。
好测试有边界
一个测试最好只回答一个问题。
如果一个测试同时断言:
- 格式正确
- 权限正确
- 网络请求正确
- toast 文案正确
- 数据库状态正确
它失败时你很难知道问题在哪。
AI 特别容易写这种“大而全”的测试,因为它想一次证明很多东西。TDD 要反过来:一个行为,一个失败,一个实现。
好测试会让实现难以作弊
如果测试只覆盖一个过于特殊的输入,AI 可能写出刚好匹配的假实现。
比如:
export function slugify(input: string) {
return "hello-world";
}第一条测试能让它过,但第二条测试就会逼出真正逻辑:
expect(slugify("Test Driven Development")).toBe("test-driven-development")所以 TDD 不是永远只写一个测试,而是每一轮只新增一个行为压力。压力逐步增加,设计逐步长出来。
五、AI 会怎么绕过 TDD

这部分必须讲清楚,因为 AI 不会天然尊重测试。
它的优化目标很简单:完成你刚才说的任务。如果你说“让测试通过”,它可能会做出一些人类不想要的动作。
第一种:改测试凑绿
最典型:
- 把
toBe("hello-world")改成当前输出 - 删除失败断言
- 给测试加
skip - 把严格断言改成宽松断言
这不是 TDD,这是把红灯拆掉。
第二种:写过拟合实现
比如测试只有一个输入:
expect(slugify("Hello World")).toBe("hello-world")模型可能写出:
if (input === "Hello World") return "hello-world";这时你不需要骂它。你需要继续加下一条行为,让实现无法继续硬编码。
第三种:用 mock 盖住真实边界
AI 很喜欢 mock。mock 让测试容易写,也让很多真实问题消失。
不是说不能 mock,而是要问:
我现在 mock 掉的,是慢依赖,还是我真正想验证的边界?
如果你要验证支付回调解析,却把解析层 mock 掉了,测试就没有意义。
六、什么时候不该用 TDD
TDD 有价值,但不是所有事情都值得套。
不适合的场景
- 纯视觉微调
- 一次性脚本
- 技术探索 demo
- 需求本身还没想清楚的原型
- 测试框架还没搭好的仓库
这些场景先追求探索速度,不要被流程拖住。
适合的场景
- bug 修复
- 权限、计费、状态机
- 数据转换和边界处理
- 会长期维护的核心模块
- AI 会反复修改的代码路径
判断标准不是“这个功能大不大”,而是:
如果它错了,代价是否明显?
代价明显,就值得先写测试。
七、一个可执行的心法
如果只给 AI 一句话,我不会说:
请高质量实现这个功能。我会说:
先写一个失败测试,运行它,确认失败原因符合预期。不要写实现,直到我说 go。这句话的质量更高,因为它不是要求模型“表现好”,而是要求它进入一个可检查的流程。
再完整一点:
每轮只处理一个行为。
RED:写一个失败测试并运行。
GREEN:写最小实现,不改测试。
REFACTOR:只在绿色状态下整理结构。
每轮报告测试文件、命令、失败原因、通过结果。这就是 AI 时代 TDD 的核心。
不是迷信测试,不是迷信流程,而是让每一步都有证据。
收尾
AI 编程最需要的不是更多代码,而是更短的反馈。
TDD 的价值就在这里:它把“我觉得应该对”变成“这里有一个失败,后来它变绿了”。这个变化很小,但足够真实。
如果你只记一句话,记这个:
不要让 AI 直接交付代码。先让它交付一个红灯,再让它把红灯变绿。
下一篇 实战指南 会把这套节奏变成可以直接复制的工作流。
推荐资源
Augmented Coding:超越氛围
Kent Beck 用 AI agent 写代码时的第一人称复盘,核心是把 vibe coding 变成工程化协作。
Red/green TDD
为什么 red/green TDD 会成为 coding agent 能理解、能执行的紧凑工作指令。
《测试驱动开发》by Kent Beck
定义红绿重构循环的经典书。理解 TDD,最终还是要回到小步反馈。