跳转到内容

测试驱动开发 (test-driven-development)

测试驱动开发(test-driven-development)

Section titled “测试驱动开发(test-driven-development)”

test-driven-development(TDD)是 Superpowers 里最“铁律型”的 Skill 之一。它不是建议,不是经验,不是“有空就做”的最佳实践,而是一个非常明确的工作顺序:

  1. 先写测试
  2. 看着它失败
  3. 再写最小实现让它通过
  4. 然后重构

它所防止的,不只是“没写测试”这么简单,而是防止一种更危险的工作习惯:

先写代码,再用测试去证明自己已经写对了。

原文明确指出:如果你没有亲眼看见测试在实现前失败,你就不能确定这个测试真正测到了你以为它在测的东西。

TDD 的流程就是经典的 Red → Green → Refactor,但原文把它执行得非常严:

graph LR
  A[RED 写失败测试] --> B{确认失败原因正确?}
  B -->|否| A
  B -->|是| C[GREEN 写最小实现]
  C --> D{确认测试通过且无额外问题?}
  D -->|否| C
  D -->|是| E[REFACTOR 重构整理]
  E --> D
  D --> F[下一轮测试]
  F --> A

要求很清楚:

  • 一次只测一个行为
  • 测试名必须清楚
  • 测真实行为,不是测 mock
  • 尽量少用 mock,除非不可避免

这一步不能跳过。 你要确认:

  • 测试真的 fail 了
  • fail 的原因是预期的(例如功能未实现)
  • 不是拼写错误、导入错误、环境错误

如果测试一开始就 pass:

  • 你测到的是已有行为
  • 或者测试写错了
  • 必须回去修测试

原文非常强调:

  • 不是“顺便做完整”
  • 不是“把未来扩展也一起做好”
  • 不是“顺手重构一下”

而是:

  • 写最少代码让这一个测试通过

同样不能跳过。 你要确认:

  • 当前测试通过
  • 其他测试也通过
  • 输出干净,没有额外 warning / error

这一步允许你:

  • 去重
  • 改命名
  • 抽 helper
  • 调整结构

但前提是:

  • 行为不变
  • 测试持续保持绿灯

何时使用

  • 新功能实现
  • Bug 修复
  • 重构
  • 行为变化

何时不用

  • 抛弃型原型(需人类明确同意)
  • 纯配置文件
  • 自动生成代码(仍建议和人确认)

关键要点

  • 没有失败测试,就没有生产代码
  • 测试必须先失败再通过
  • 实现必须最小化
  • 先绿灯,再重构

常见错误

  • 先写实现再补测试
  • 测试一开始就通过
  • 用 mock 测 mock
  • 借实现代码“参考着写测试”
英文术语中文翻译解释
TDD测试驱动开发用测试来定义行为,并强制实现按行为收敛。
RED红灯阶段先写失败测试,证明测试确实测得到目标行为。
GREEN绿灯阶段写最小实现让测试通过。
REFACTOR重构阶段在测试保持通过的前提下清理代码结构。
Iron Law铁律没有失败测试,就不允许写生产代码。
Rationalization自我合理化用“这次特殊”“太简单了”等借口跳过 TDD。
错误行为为什么是错的正确做法
“我先写出来,再补测试”测试会被实现结果反向影响先写测试,再写实现
测试一开始就 pass证明不了它真的测到了行为回去修测试,直到正确 fail
把已有实现保留作“参考”本质上还是 tests-after删除实现,按测试重新写
手工测过了就不写测试手工验证不可复用、不可回归让 bug/行为进入自动测试
测试太难写,就先略过往往说明设计本身太复杂简化接口或重新设计

这份 Skill 的语气非常强硬,甚至近乎“宗教化”,比如:

  • 写了实现?删掉,重来
  • tests-after 不等于 TDD
  • “保持作参考”也不行
  • “这次特殊”也是借口

这种强度不是为了形式感,而是为了防止开发者滑回最常见的模式:

先写代码,再想办法给它找测试。

因为一旦进入这种顺序,测试很容易变成:

  • 验证自己已经写出来的东西
  • 而不是定义应该实现什么

原文反复强调顺序,是在保护测试的“约束力”。

程序员在真实工作中很容易说:

  • 这个太简单了
  • 我先修了再说
  • 我手工测一下就行
  • 测试最后补

这类说法的共同点是: 它们看起来节省时间,但通常会把时间花到后面更贵的地方:

  • debug
  • 回归 bug
  • 重构不敢动
  • 线上异常

TDD 并不是让你慢,而是把时间前置到“定义行为”上,减少后面的修补和不确定性。

错误做法:

  • 直接写 retry function
  • 再补一个“应该能工作”的测试

正确做法:

  • 先写测试:失败两次,第三次成功
  • 看它 fail
  • 再写最小实现让它 pass

错误做法:

  • 直接在代码里加一个 if

正确做法:

  • 先写测试复现空 email 被接受的问题
  • 看它 fail
  • 再修实现
  • 让测试变绿,防止回归

错误做法:

  • 直接改结构
  • 改完再补一批测试

正确做法:

  • 先补现有行为测试
  • 确认都能 fail/pass 对应正确
  • 再开始重构
❌ 错误:先写实现代码,再补测试
✅ 正确:先写失败测试,运行确认失败,再写最小实现
❌ 错误:直接修改代码让它看起来能工作
✅ 正确:先写测试复现 Bug,确认测试失败,再修复
Terminal window
# RED 阶段:写失败测试
npm test -- --testNamePattern="描述测试名称"
# 验证测试失败
# expected error, got undefined
# GREEN 阶段:写最小实现
# 让测试通过
# REFACTOR 阶段:重构
# 保持测试通过的前提下优化代码
表现解决
先写实现再补测试测试被实现反向影响删除实现,从测试重新开始
测试直接通过没验证测试真测到行为确认测试正确 fail
”这次例外”跳过 TDD 流程没有任何例外
  • 前置using-superpowers,用于决定当前是否该进入 TDD;writing-plans,用于在复杂实现前先把计划写清楚。
  • 后续verification-before-completion,因为 TDD 并不替代最终验证。
  • 互补systematic-debugging,用于当 bug 尚未被准确理解时,先定位问题,再写失败测试复现。

这份附属文档把很多“看起来像在写测试,其实没有真正验证行为”的问题讲得非常透。

它重点批评了几类常见反模式:

1. 测 mock 的行为,而不是测真实行为

Section titled “1. 测 mock 的行为,而不是测真实行为”

比如断言一个 mock sidebar 被渲染出来,这实际上只证明 mock 存在,不证明真实组件行为正确。

2. 在生产代码里添加 test-only 方法

Section titled “2. 在生产代码里添加 test-only 方法”

比如为了测试方便,在生产类里加 destroy() 这种只给测试用的方法。原文认为这会污染生产 API。

为了“安全”或“省事”把高层依赖都 mock 掉,结果把测试真正依赖的副作用也一起抹掉了。

只 mock 你当前测试用到的字段,而不按真实 API 结构模拟,最后造成“测试通过、集成失败”。

这份文档的真正价值在于提醒你:

测试不是为了让 CI 变绿,而是为了验证真实系统行为。

所以 mock 是工具,不是主角;测试结构也必须服务于真实行为验证。


查看源文件: GitHub原始文件