“要先写测试?代码都还没有,到底要测试什么?”
第一次接触 TDD(Test-Driven Development,测试驱动开发)时,所有人都会这么想。顺序看起来反了,还会觉得工作量莫名其妙增加了一倍。
不过,实际投入几个月后,想法会稍微改变。
TDD 是一种反复执行极短循环的开发方式:“失败的测试(Red)→ 让测试通过的最少代码(Green)→ 整理(Refactor)”。关键是以几秒到几分钟的速度快速跑完这个循环。
今天就一步步看看这个 Red–Green–Refactor 循环究竟如何运转。
TDD Red–Green–Refactor:3 个步骤重点总结 ✍️
先一眼了解整个循环。
- Red:先为尚不存在的功能编写测试。自然会失败。
- Green:只编写让测试通过所需的最少代码。不必漂亮。
- Refactor:在保持测试通过的同时整理代码。
把这三个步骤合在一起持续重复,就全部完成了。
重要的是不要把每个步骤定得太大。不要一次处理“完整的登录功能”,而要拆成“电子邮件为空时返回错误”这样的细小任务。
如果一个循环需要 30 分钟,那很可能只是加入了测试的开发,而不是 TDD。
Red 阶段,为什么要故意编写会失败的代码?
这是学习 TDD 时最难理解的部分。明知道会失败,为什么还要运行?
原因很简单。为了确认这个测试“确实会失败”。
如果测试刚写好就通过了,它实际上可能只是一个什么都没验证的空壳。先看到失败,就相当于测试测试本身。
举个简单的例子,假设要创建一个把两个数字相加的函数。测试会先于代码出现。
// add 函数尚不存在。因此,这个测试会失败(Red)
struct AddTests {
@Test func 加法_1相加2是3等于() {
#expect(add(1, 2) == 3)
}
}
在这种状态下运行,会出现类似“cannot find ‘add’ in scope”的错误。反而是看到这个红灯让我更安心,因为目标变得明确了。
从现在开始,我要做的只有一件事:把红灯变成绿灯。
Green 和 Refactor,真正的实力就在这里见分晓
Green 阶段的原则是“简单到令人不好意思”。
让前面的测试通过,最快的代码就是这样。
// 让测试通过所需的最少代码(Green)
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
看起来太理所当然了,对吧?但初学者往往会在这里担心未来,提前加入异常处理、日志和配置值。
TDD 会故意让你忍住这个冲动。规则就是:不编写当前测试没有要求的代码。
等绿灯亮起后,才进入 Refactor。这时可以调整变量名,移除重复代码。
关键是,Refactor 期间绝不添加新功能。只在保持测试通过的状态下改进结构。
如果整理时亮起红灯怎么办?把刚才修改的内容撤销即可。测试会保证它刚才还正常工作。
多亏了这张安全网,我重构时变得大胆得多。
使用 TDD,开发不会变慢吗? 🤔
这是我最常被问到的问题。实际使用后,我的答案是:“要看情况。”
我把自己感受到的差异整理成了表格。
| 分类 | 应用 TDD | 之后再写测试 |
|---|---|---|
| 初期速度 | 稍慢 | 快 |
| 发现错误的时机 | 编写后立即 | 很久以后 |
| 重构负担 | 低 | 高 |
| 复杂逻辑 | 有利 | 不利 |
老实说,快速制作一个简单页面时,TDD 有时会让人觉得碍手碍脚。
相反,在支付、结算、折扣计算等条件交织的逻辑中,它确实发挥了作用。用测试逐一锁定各种情况后,即使之后大幅修改代码,也不再害怕。
归根结底,TDD 不是万能药,更像是一种“管理复杂性的工具”。
总结
如果觉得 TDD 很难,不要一开始就做得很宏大,先用一个函数跑一遍 Red–Green–Refactor。
当小小的成功循环变成习惯,你就会稍微理解为什么人们无法放弃这种方法。今天就从一个测试开始点亮红灯吧。

