概述
该技能一句话总结为:TDD 循环——先写一个失败测试(红),写最少代码让其通过(绿),在保持通过的同时重构代码;一次只走一小步。纪律体现在顺序和“最小实现”上,而这两点都很容易被跳过。
适用场景
在能够用测试表达期望结果的代码中构建新行为时使用。缺陷修复时,先写回归测试再修复。从调用方角度设计 API,第一个测试会迫使你考虑调用代码想要什么。在测试套件健康、TDD 阻力较小的代码库中工作。
不适用场景
纯粹探索/技术验证(spike)阶段,尚不清楚要构建什么形态时;先做 spike,再对重写内容做测试驱动。一次性脚本、一次性工具或不会维护的快速原型。UI 微调,即“写测试、改像素、看测试通过”的循环弊大于利时。在完全没有测试基础设施的代码库中,为单个功能搭建测试框架成本不成比例时;应添加测试,而不是假装在做 TDD。
核心内容
红——写一个失败测试。为尚不存在(或即将添加)的行为写一个小测试,运行它,确认它以正确的原因失败:是编译错误或预期行上的断言失败,而不是三层之外某个随机的 NullPointerException。
绿——让测试通过,用最少所需代码。只写刚好能让测试变绿的生产代码。最小实现是关键:此刻可以采用任何“权宜之计”(例如直接返回硬编码值没问题)。最小实现是检验测试是否正确接线的最便宜信号。
重构——在保持绿色的同时清理代码。有了通过的测试网,重构代码:提取方法、重命名变量、消除重复。每次小改动后运行测试。若变红,说明重构做错了,应撤销并尝试更小的改动。
为什么顺序重要
先红迫使你写一个会失败的测试。若先写生产代码,后面补的测试可能根本不是必需的。最小绿是循环的纪律;没有它,就会过度设计,循环不再紧凑。带测试网的重构才是设计改进的时机:没有测试,你害怕改代码;有测试,你可以实验。
循环长度
一个完整循环应以分钟计,而不是小时。若一个循环超过约 30 分钟,说明测试太大,应求助 tdd-child-test。
TDD 不是“写测试”。后写测试是测试;先写测试是设计。顺序改变了代码如何生长;两者都产出测试,但只有一种驱动设计。
判断准则
每个循环步骤都运行测试,不要批量运行,快速反馈才是重点。新功能的第一个测试应小且快速,大或慢的首个测试会杀死循环。只在绿色时重构;在红色时重构是“在损坏状态下改代码”,已不再是 TDD。如果发现自己先写生产代码再写测试,测试会偏向已写好的实现,应停下来回退。
反模式
代码之后才写测试:常被没真正做过 TDD 的人称为 TDD,它不驱动设计。大测试覆盖大范围:慢、难以变绿、失败时信息量低。在红色时重构:“我在修设计,等完成时测试也会变绿”,混淆了两件事,两者都会出错。跳过最小绿步骤:从红直接跳到“真实”实现,常引入测试并不需要的复杂度。把 TDD 当宗教:对无收益的代码(一次性脚本、探索性 spike)坚持 TDD;纪律成本真实存在,应花在有用之处。
参见
tdd-identify(陷入循环时路由到正确技术);tdd-fake-it、tdd-obvious-implementation、tdd-triangulate(完成“绿”步骤的三种方式);tdd-child-test、tdd-one-step-test(红测试太大或无法选择时);tdd-anti-patterns(意味着 TDD 出问题的坏味道);Superpowers' superpowers:test-driven-development(工作流级触发)。
参考资料
Kent Beck,《Test-Driven Development By Example》(Addison-Wesley,2002)——经典参考,第一部分(Money 示例)走过数十个红-绿-重构循环。Beck 的 “TDD by Example” 演讲(经常进行)——同内容的精简版本。