测试驱动开发 tdd

测试驱动开发是一种严格的软件开发方法学,强调先编写失败的单元测试,再编写最少量的实现代码使其通过,最后进行重构优化。其核心是红绿重构循环,旨在提升代码质量、可维护性和设计。关键词:TDD,测试驱动开发,红绿重构,单元测试,软件设计,敏捷开发,代码质量,重构。

测试 0 次安装 0 次浏览 更新于 3/2/2026

name: tdd description: 测试驱动开发工作流 - 先写测试,再写实现

测试驱动开发 (TDD)

严格遵守红-绿-重构循环。没有失败的测试之前,绝不编写生产代码。

TDD 循环

┌─────────────────────────────────────────┐
│                                         │
│    ┌───────┐                           │
│    │  红   │ 编写一个失败的测试       │
│    └───┬───┘                           │
│        │                               │
│        ▼                               │
│    ┌───────┐                           │
│    │  绿   │ 编写最少代码使其通过     │
│    └───┬───┘                           │
│        │                               │
│        ▼                               │
│    ┌──────────┐                        │
│    │  重构   │ 在不破坏测试的前提下改进│
│    └────┬─────┘                        │
│         │                              │
│         └──────────────────────────────┘

规则 (不可协商)

  1. 没有失败的测试,就没有生产代码

    • 先写测试
    • 看着它失败
    • 然后才写实现
  2. 编写能失败的最简测试

    • 每个测试一个断言
    • 测试行为,而非实现
    • 从最简单的情况开始
  3. 编写能通过的最简代码

    • 不要预判未来需求
    • 如果硬编码能让测试通过,就硬编码
    • 只有在被测试逼迫时才进行泛化
  4. 只在绿灯时重构

    • 重构前所有测试必须通过
    • 重构过程中保持测试通过
    • 只进行小步修改

工作流程

阶段 1: 红 (编写失败测试)

// 从你期望的 API 样子开始
describe('计算器', () => {
  it('应该能将两个数字相加', () => {
    const calc = new Calculator();
    expect(calc.add(2, 3)).toBe(5);
  });
});

运行测试 → 看着它失败 → 确认它因正确的理由而失败

阶段 2: 绿 (使其通过)

// 编写能通过的最简单代码
class Calculator {
  add(a: number, b: number): number {
    return 5; // 是的,这里硬编码完全可以!
  }
}

运行测试 → 看着它通过 → (短暂地)庆祝

阶段 3: 添加另一个测试 (迫使泛化)

it('应该能加不同的数字', () => {
  const calc = new Calculator();
  expect(calc.add(1, 1)).toBe(2); // 迫使真正的实现
});

阶段 4: 再次变绿

class Calculator {
  add(a: number, b: number): number {
    return a + b; // 现在我们进行泛化
  }
}

阶段 5: 重构

在测试通过的情况下,改进代码:

  • 消除重复
  • 改进命名
  • 提取方法
  • 始终保持测试通过

测试命名约定

使用此模式:当 [条件] 时,应该 [预期行为]

it('当输入为空时,应该返回空数组')
it('当邮箱无效时,应该抛出验证错误')
it('当连接失败时,应该重试3次')

TDD 检查清单

在编写任何代码之前,确认:

  • [ ] 我有一个失败的测试
  • [ ] 测试因预期原因而失败
  • [ ] 测试的是行为,而非实现
  • [ ] 测试名称描述了它要验证的内容

在标记为“绿”之前:

  • [ ] 测试通过
  • [ ] 我编写了必要的最少代码
  • [ ] 我没有添加“额外”功能

在重构之前:

  • [ ] 所有测试都是绿的
  • [ ] 我心中有一个具体的改进目标
  • [ ] 更改是小步且渐进的

常见的 TDD 错误

❌ 先写代码后写测试

这不是 TDD。事后编写的测试往往测试的是实现,而非行为。

❌ 一次编写太多测试

编写一个测试,让它通过,然后写下一个。保持在循环中。

❌ 步子迈得太大

如果你的实现超过几行代码,说明你跳过了步骤。添加中间测试。

❌ 测试实现细节

// 不好 - 测试实现
expect(user._hashedPassword).toMatch(/^[a-f0-9]{64}$/);

// 好 - 测试行为
expect(user.verifyPassword('correct')).toBe(true);

❌ 在红灯时重构

永远不要在测试失败时重构。先变绿。

使用 TDD 开始一个新功能

  1. 列出功能所需的行为 (用户故事 → 测试用例)
  2. 按从最简单到最复杂的顺序排列
  3. 为最简单的行为编写第一个测试
  4. 循环执行红-绿-重构
  5. 为边界情况添加测试
  6. 为错误处理添加测试

示例会话

目标: 实现一个 slugify 函数

// 测试 1: 最简单的情况
it('应该将输入转为小写', () => {
  expect(slugify('Hello')).toBe('hello');
});
// 实现:return input.toLowerCase();

// 测试 2: 处理空格
it('应该用连字符替换空格', () => {
  expect(slugify('Hello World')).toBe('hello-world');
});
// 实现:return input.toLowerCase().replace(/ /g, '-');

// 测试 3: 处理特殊字符
it('应该移除特殊字符', () => {
  expect(slugify('Hello, World!')).toBe('hello-world');
});
// 实现:添加 .replace(/[^a-z0-9-]/g, '');

// 测试 4: 边界情况
it('应该处理空字符串', () => {
  expect(slugify('')).toBe('');
});
// 已经通过!无需更改。

记住

“TDD 不是关于测试。它是关于设计的。”

测试驱动你走向更好、更模块化的代码。相信这个过程。