tdd

产品介绍

tdd 是 Matt Pocock Skills 中「修 bug 流水线」的第 3 步,是一个测试驱动开发(Test-Driven Development)Skill。安装在 ~/.trae-cn/skills/tdd/SKILL.md,当用户说「测试驱动开发」时 AI 自动调用。核心是红-绿-重构循环。

什么是 TDD

TDD 不是「先写代码,事后补测试」。它的思路正好相反:

传统开发:需求 → 设计 → 写代码 → 手动测试 → 补自动化测试 TDD:需求 → 写测试(测试先失败)→ 写代码让测试通过 → 重构

TDD 的核心洞察是:先写测试,你才知道自己要写什么代码。就像先写一份验收标准,再按标准实现。

红-绿-重构循环

这是 TDD 的三个阶段,不断循环:

红(Red)

做什么:写一个会失败的测试。

为什么先让它失败:如果测试一开始就通过,说明它没测什么需要测的东西,或者测的东西已经存在了。失败的测试证明:测试真的在测你想测的东西。

完成标准:测试运行,失败,失败信息清晰。

例子:

test('除以零应该抛出错误', () => {
  expect(() => divide(10, 0)).toThrow();
});
// 运行 → 红:divide 函数还不存在,或者没有抛出错误

绿(Green)

做什么:写最少的代码让测试通过。

关键:是「最少」的代码,不是「最好」的代码。

为什么最少:这个阶段的目标是让测试通过,不是完美实现。写得越少,越不容易引入不必要的复杂度。完美主义在重构阶段再来。

完成标准:测试运行,通过。

例子:

// 最少代码让上面的测试通过
function divide(a, b) {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
}
// 运行 → 绿:测试通过

重构(Refactor)

做什么:优化代码结构,保持测试通过。

为什么:刚写的代码通常很粗糙(一个 if-else 就搞定了),但这样的代码久了会难以维护。重构让它保持可读性和可维护性,同时测试保证你不会破坏功能。

完成标准:测试仍然全部通过,代码更清晰。

例子:

// 重构前
function divide(a, b) {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
}

// 重构后(提取错误处理,分离关注点)
function divide(a, b) {
  validateNotZero(b);
  return a / b;
}

function validateNotZero(value) {
  if (value === 0) throw new Error('Cannot divide by zero');
}

核心原则

1. 垂直切片

一次只做一个功能的一小部分,不要一次写完所有测试。

为什么:垂直切片让你每次循环都能得到一个可运行的、端到端的成果。水平切片(先写完所有测试再写实现)会让你在很长时间内看不到任何成果,而且容易产生垃圾测试。

反例(水平切片):

写用户登录的所有测试 → 写用户登录的所有代码 → 运行
(中间有很长一段时间测试全是红的,容易放弃)

正例(垂直切片):

写「用户名为空」的测试 → 写代码让测试通过 → 重构
写「密码为空」的测试 → 写代码让测试通过 → 重构
写「用户名密码都正确」的测试 → 写代码让测试通过 → 重构
(每次循环都有一个能跑的版本)

2. 测试行为,不测试实现

测试应该描述系统「做什么」,而不是「怎么做」。

为什么:测试实现细节会导致重构时测试频繁挂掉。你重构就是为了改实现,如果测试绑在实现上,重构就举步维艰。

反例(测试实现):

test('应该调用 saveToLocalStorage', () => {
  // mock 内部函数,验证它被调用了
});
// 重构:换一种存储方式 → 测试挂了 → 重构失败

正例(测试行为):

test('用户数据应该被保存', () => {
  // 保存用户数据
  saveUser(userData);
  // 验证数据能被读回来
  expect(readUser()).toEqual(userData);
});
// 重构:换一种存储方式 → 测试仍然通过

3. 永远不要在红的时候重构

先让测试通过(绿),再优化代码(重构)。

为什么:在测试失败时重构,你不知道是代码有问题还是测试有问题。先变绿,说明测试和代码达成一致,这时重构才是安全的。

反模式

水平切片

先写完所有测试,再写完所有实现。

问题:

  • 长时间没有成果,容易放弃
  • 测试容易变成「为了写测试而写测试」,没有实际价值
  • 发现问题时已经写了很多代码,改动成本高

测试实现细节

测试内部函数、mock 内部协作者。

问题:

  • 重构时测试会挂
  • 测试变得脆弱,频繁误报失败
  • 团队开始忽视测试

假绿

测试看起来通过了,但实际上什么都没测。

常见原因:

  • mock 了太多东西,测试的不是真实行为
  • 断言太弱(比如只断言函数不抛错)
  • 测试数据是假的,和真实场景无关

使用场景

  • 修 bug 时,先写回归测试:diagnosing-bugs 第 5 步就是这个
  • 开发新功能时,先写测试再写实现
  • 重构旧代码时,先写测试保护现有行为

与上下游的关系

  • 上游:diagnosing-bugs 诊断循环的第 5 步(修 + 回归测试)用的就是 TDD 方法
  • 本身:TDD 循环可以反复迭代,每次循环产出一个可运行的小版本

相关笔记

相关日记