1Growing Object-Oriented Software, Guided by Tests

测试驱动开发是什么

TDD的核心思想

红-绿-重构测试先行设计工具反馈循环

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第1章:测试驱动开发是什么(What Is TDD?)

一、导读

1.1 本章学习目标

第1章是《Growing Object-Oriented Software, Guided by Tests》的开篇,Freeman 和 Pryce 以他们丰富的实践经验,阐述了测试驱动开发(TDD)的真正含义。TDD 不仅仅是"先写测试再写代码",它是一种设计方法,一种思维模式。

本章的核心学习目标包括:

  • 理解 TDD 的核心思想:红-绿-重构循环。
  • 认识 TDD 作为设计工具的价值:测试如何驱动设计决策。
  • 理解测试的反馈循环:测试如何提供快速、可靠的反馈。
  • 掌握 TDD 的节奏:小步前进,频繁验证。
  • 区分 TDD 和"测试后开发":理解先写测试的重要性。

1.2 为什么第一章如此重要

很多人对 TDD 有误解,认为它只是"先写测试"。但 Freeman 和 Pryce 在本章就指出,TDD 的真正价值在于设计——测试是设计的工具,而不仅仅是验证的工具。

这一章奠定了全书的基调:TDD 不是关于测试的,而是关于设计的。测试只是手段,好的设计才是目的。

1.3 核心问题引导

TDD 的核心循环是什么?每一步的目的是什么?

TDD 如何帮助设计?测试如何揭示设计问题?

为什么"先写测试"如此重要?

TDD 的节奏是什么?为什么小步前进很重要?

TDD 和代码质量有什么关系?


二、核心概念详解

2.1 TDD 的核心循环

TDD 的核心是一个简单的循环:红-绿-重构。

红灯(Red):写一个失败的测试
    ↓
绿灯(Green):写最少的代码让测试通过
    ↓
重构(Refactor):改善代码结构,保持测试通过
    ↓
回到红灯

2.1.1 红灯:写一个失败的测试

红灯阶段的目标是明确你要实现什么。测试描述了期望的行为,在代码实现之前,测试应该失败。

java
// 红灯:测试失败
@Test
public void shouldReturnSumOfTwoNumbers() {
    Calculator calc = new Calculator();
    assertEquals(5, calc.add(2, 3));
}
// 编译失败:Calculator 类还不存在

2.1.2 绿灯:写最少的代码让测试通过

绿灯阶段的目标是让测试通过,但不是写出完美的代码。关键是快速到达绿灯,获得反馈。

java
// 绿灯:测试通过
public class Calculator {
    public int add(int a, int b) {
        return 5;  // 最少的代码,让测试通过
    }
}

2.1.3 重构:改善代码结构

重构阶段的目标是改善代码的结构和质量,同时保持测试通过。

java
// 重构:改善实现
public class Calculator {
    public int add(int a, int b) {
        return a + b;  // 正确的实现
    }
}

2.2 TDD 作为设计工具

TDD 的真正价值不在于测试,而在于设计。测试是设计的工具:

测试揭示设计问题:如果测试难以编写,说明设计有问题。

测试驱动接口设计:从使用者的角度设计 API。

测试验证设计决策:测试通过意味着设计是正确的。

java
// 测试难以编写 → 设计有问题
@Test
public void shouldSendNotification() {
    // 需要创建太多依赖对象
    // 说明设计耦合太紧
}

// 重构设计
@Test
public void shouldSendNotification() {
    NotificationService service = new NotificationService(mockEmailSender);
    service.notify("user123", "Hello");
    verify(mockEmailSender).send("user123", "Hello");
}
// 测试容易编写 → 设计更清晰

2.3 测试的反馈循环

TDD 提供快速、可靠的反馈循环:

编写测试(1-2分钟)
    ↓
运行测试,确认失败(几秒钟)
    ↓
编写实现(1-5分钟)
    ↓
运行测试,确认通过(几秒钟)
    ↓
重构(1-5分钟)
    ↓
运行测试,确认仍然通过(几秒钟)
    ↓
循环

这个反馈循环的关键是速度。每次循环应该在几分钟内完成。如果循环太慢,就失去了 TDD 的价值。

2.4 TDD 的节奏

TDD 的节奏是小步前进:

小步前进:每次只实现一小部分功能。

频繁验证:每实现一小部分就运行测试。

持续重构:不断改善代码结构。

大步骤(危险):
- 写10个测试
- 写大量代码
- 运行测试
- 大量失败
- 难以定位问题

小步骤(安全):
- 写1个测试
- 写最少的代码
- 运行测试
- 通过
- 重构
- 写下一个测试

2.5 TDD vs 测试后开发

TDD 和"先写代码再补测试"有本质区别:

维度TDD测试后开发
测试目的设计工具验证工具
测试时机代码之前代码之后
测试覆盖率高(测试驱动设计)低(难以测试的代码被跳过)
设计质量高(测试揭示设计问题)低(测试适应现有设计)
反馈速度快(几分钟)慢(几小时或几天)
重构信心高(测试保护)低(测试不充分)

三、实战案例

3.1 用 TDD 实现字符串计算器

java
// 第1步:红灯
@Test
public void shouldReturnZeroForEmptyString() {
    assertEquals(0, StringCalculator.add(""));
}

// 绿灯
public class StringCalculator {
    public static int add(String numbers) {
        return 0;
    }
}

// 第2步:红灯
@Test
public void shouldReturnNumberForSingleNumber() {
    assertEquals(1, StringCalculator.add("1"));
    assertEquals(2, StringCalculator.add("2"));
}

// 绿灯
public class StringCalculator {
    public static int add(String numbers) {
        if (numbers.isEmpty()) return 0;
        return Integer.parseInt(numbers);
    }
}

// 第3步:红灯
@Test
public void shouldReturnSumForTwoNumbers() {
    assertEquals(3, StringCalculator.add("1,2"));
}

// 绿灯
public class StringCalculator {
    public static int add(String numbers) {
        if (numbers.isEmpty()) return 0;
        String[] parts = numbers.split(",");
        int sum = 0;
        for (String part : parts) {
            sum += Integer.parseInt(part);
        }
        return sum;
    }
}

// 重构
public class StringCalculator {
    public static int add(String numbers) {
        if (numbers.isEmpty()) return 0;
        return Arrays.stream(numbers.split(","))
            .mapToInt(Integer::parseInt)
            .sum();
    }
}

3.2 TDD 揭示设计问题

java
// 初始设计:测试难以编写
@Test
public void shouldCreateOrder() {
    // 需要创建数据库连接、用户服务、库存服务...
    // 测试变得复杂且脆弱
}

// TDD 揭示问题:依赖太多,耦合太紧
// 重构设计:引入依赖注入

@Test
public void shouldCreateOrder() {
    OrderService service = new OrderService(
        mockUserRepo, mockInventoryService, mockPaymentGateway);
    
    Order order = service.createOrder("user123", List.of("item1", "item2"));
    
    assertEquals("user123", order.getUserId());
    assertEquals(2, order.getItems().size());
    verify(mockInventoryService).reserve("item1");
    verify(mockInventoryService).reserve("item2");
}
// 测试容易编写 → 设计更清晰,依赖可替换

四、本章小结

第1章阐述了 TDD 的核心思想:

红-绿-重构:TDD 的核心循环,小步前进,频繁验证。

TDD 是设计工具:测试驱动设计,而不仅仅是验证。

快速反馈:每次循环应该在几分钟内完成。

小步前进:每次只实现一小部分功能。

先写测试:从使用者的角度设计 API。

TDD 不是关于测试的,而是关于设计的。测试只是手段,好的设计才是目的。


五、思考与练习

思考题

TDD 的核心价值是什么?是测试还是设计?

为什么"先写测试"比"后补测试"更好?

TDD 的小步前进有什么优势?

如果测试难以编写,说明了什么?

TDD 适合所有场景吗?有什么限制?

编程练习

用 TDD 实现 FizzBuzz:从最简单的情况开始,逐步增加复杂度。

用 TDD 实现栈:先写测试描述栈的行为,再实现栈。

用 TDD 重构一段代码:先为现有代码补充测试,再重构。


1/7测试驱动
0%