4、测试驱动开发(TDD):红绿重构循环、TDD三定律、实战案例
测试驱动开发,简称 TDD。很多人觉得它就是个「先写测试再写代码」的流程。说实话,我早年也这么想。直到我在一个核心支付项目上被坑惨了——代码写了两周,一跑全是 bug,改了一个又冒出三个。后来我硬着头皮把整个模块推倒重来,用 TDD 重新写了一遍。嗯,那次之后,我再也不敢小看这个「红绿重构」的循环了。
TDD 不是测试技巧,它是一种设计方法论。它逼着你在写代码之前,先想清楚「这段代码到底要干什么」。你想想看,这比写完再补测试,靠谱多了。
红绿重构循环:TDD 的核心节奏
TDD 的循环只有三步,但每一步都有讲究。
- 红(Red):先写一个会失败的测试。这个测试描述了你想要的新功能。运行它,必须是红的。
- 绿(Green):写最少的代码让测试通过。别管代码多丑,能过就行。
- 重构(Refactor):在测试的保护下,优化代码结构。去掉重复,提升可读性。
循环往复,每次只前进一小步。我个人的习惯是,如果一次循环超过 5 分钟,说明步子迈大了。
核心要点:红绿重构不是「写完代码再补测试」,而是「用测试驱动代码生长」。每一步都有明确的退出条件——测试全绿。
为什么会这样设计?因为人脑一次能处理的信息有限。把「写代码」和「验证代码」拆成两个独立步骤,反而更快。我在项目中遇到过很多次,一上来就写实现,结果写到一半发现需求理解错了,全白干。TDD 的「红」阶段,就是帮你提前发现这种理解偏差。
TDD 三定律:Uncle Bob 的铁律
Robert C. Martin(大家叫他 Uncle Bob)给 TDD 定了三条硬规矩。我刚开始觉得太死板,后来发现,这三条每一条都是用血泪换来的。
| 定律 | 内容 | 我的解读 |
|---|---|---|
| 第一定律 | 在编写生产代码之前,先编写失败的单元测试。 | 没有测试,就不要动实现代码。这是纪律。 |
| 第二定律 | 只编写刚好让测试失败(编译不通过也算)的单元测试。 | 别贪多。一次只测一个行为。 |
| 第三定律 | 只编写刚好让测试通过的生产代码。 | 别提前设计。够用就行,重构时再优化。 |
我曾经踩过的坑:第二定律和第三定律最容易破。我见过有人一次写了 10 个测试,然后花一下午写实现。结果测试全红,根本不知道哪个先修。正确的做法是:写一个测试,让它绿,再写下一个。一步一个脚印。
这三条定律合在一起,形成了一个极短的反馈循环。你每写几行代码,就能得到一次验证。这种节奏感,是 TDD 的精髓。
实战案例:一个简单的订单折扣计算器
光说不练假把式。我们来看一个真实的例子。假设我们要实现一个功能:根据订单金额计算折扣。规则很简单——满 100 打 9 折,满 200 打 8 折。
按照 TDD 的节奏,我们先写第一个测试:
// 测试:金额小于 100,不打折
@Test
public void should_return_original_price_when_amount_less_than_100() {
DiscountCalculator calculator = new DiscountCalculator();
double result = calculator.calculate(50.0);
assertEquals(50.0, result, 0.01);
}
运行测试,红。好,现在写最少的代码让它绿:
public class DiscountCalculator {
public double calculate(double amount) {
return amount;
}
}
测试通过。绿了。现在写第二个测试:
// 测试:金额满 100,打 9 折
@Test
public void should_apply_10_percent_discount_when_amount_is_100() {
DiscountCalculator calculator = new DiscountCalculator();
double result = calculator.calculate(100.0);
assertEquals(90.0, result, 0.01);
}
红。改实现:
public class DiscountCalculator {
public double calculate(double amount) {
if (amount >= 100) {
return amount * 0.9;
}
return amount;
}
}
两个测试都绿。继续写第三个测试:
// 测试:金额满 200,打 8 折
@Test
public void should_apply_20_percent_discount_when_amount_is_200() {
DiscountCalculator calculator = new DiscountCalculator();
double result = calculator.calculate(200.0);
assertEquals(160.0, result, 0.01);
}
红。改实现:
public class DiscountCalculator {
public double calculate(double amount) {
if (amount >= 200) {
return amount * 0.8;
}
if (amount >= 100) {
return amount * 0.9;
}
return amount;
}
}
三个测试全绿。现在进入重构阶段。看看代码,有没有重复?有没有可以提取的地方?目前逻辑清晰,暂时不需要动。但如果后续规则变复杂,我会考虑用策略模式替换 if-else。
小技巧:重构阶段不要引入新功能。只改结构,不改行为。测试就是你的安全网——只要测试全绿,重构就没问题。
TDD 的节奏感与知识体系
下面这张图,是我自己总结的 TDD 核心知识体系。它把红绿重构循环、三定律、以及实战中的关键点串在了一起。
避坑指南:我踩过的 TDD 坑
TDD 看起来简单,但实际用起来,有几个坑特别容易掉进去。
- 测试写得太粗:一个测试测了多个行为。一旦失败,你根本不知道哪里出了问题。我建议一个测试只验证一个逻辑分支。
- 跳过重构阶段:很多人让测试绿了就直接提交。代码越来越乱,最后连测试都救不了。我曾经在一个项目上接手过这样的代码——测试全绿,但代码根本没法维护。重构不是可选项,是必选项。
- 测试与实现耦合太紧:测试里直接用了私有方法或内部状态。一重构,测试全挂。正确的做法是:只测公开接口,不测实现细节。
- 追求 100% 覆盖率:覆盖率是结果,不是目标。我见过有人为了凑覆盖率,写了大量无意义的测试。说白了,那是自欺欺人。重点应该放在核心逻辑和边界条件上。
我的习惯:每次提交前,我会问自己三个问题:1)这个测试真的能捕获回归吗?2)代码有没有明显的坏味道?3)如果半年后有人来看这段代码,他能看懂吗?三个问题都回答「是」,我才提交。
TDD 不是银弹,但它确实能帮你写出更干净、更可靠的代码。关键在于坚持那个「红绿重构」的节奏,别贪快,别跳步。你试试看,坚持两周,你会发现自己写代码的方式完全变了。