SuperAPI AI 工具集

test-driven-development

测试与质量

查看项目

更新于 10-15 10:24

技能介绍

测试驱动开发(TDD)是一种先写测试、再写实现的开发方法。本技能的核心原则是:如果你没有亲眼看到测试失败,你就不知道它测的是不是正确的东西。违反规则的文字就是违反规则的精神。

用途 适用于任何功能实现或缺陷修复,在编写实现代码之前使用。始终适用于:新功能、缺陷修复、重构、行为变更。例外情况(需询问你的人类伙伴):一次性原型、生成的代码、配置文件。

铁律 没有失败的测试,就不写生产代码。如果你在测试之前写了代码,删除它,重新开始。没有例外:不要把它留作「参考」,不要在写测试时「改编」它,不要看它。删除就是删除,从测试重新实现。

红-绿-重构循环

  1. RED(红):写一个最小的失败测试,展示应该发生什么。要求:一个行为、清晰的名称、真实代码(除非不可避免否则不用 mock)。
  2. 验证 RED:强制步骤,永不跳过。运行测试(如 npm test path/to/test.test.ts),确认:测试失败(不是出错)、失败消息符合预期、因功能缺失而失败(不是拼写错误)。如果测试通过,说明你在测试已有行为,请修正测试;如果测试出错,修复错误并重跑直到正确失败。
  3. GREEN(绿):写最简单的代码让测试通过。不要添加功能、重构其他代码或超出测试范围「改进」。
  4. 验证 GREEN:强制步骤。确认测试通过、其他测试仍通过、输出干净(无错误、警告)。「其他测试」指整个项目测试套件,不只是你的文件。即使任务只指定一个测试文件,也要运行项目测试命令(如裸 pytest、npm test、cargo test)。范围内任何失败(包括非你导致的)都要在报告中点名。你看着失败的测试滚动而过却不提及,就是因遗漏而虚假报告。
  5. REFACTOR(重构):仅在绿之后进行。移除重复、改进名称、提取辅助函数。保持测试绿色,不要添加行为。
  6. 重复:为下一个功能写下一个失败测试。

好测试的标准

  • 最小:只测一件事。名称里有「and」就拆分。
  • 清晰:名称描述行为,例如 test('test1') 是坏例子。
  • 展示意图:展示期望的 API,不要掩盖代码应该做什么。 编写或修改任何测试时,要阅读 writing-good-tests.md 中的规则以保持测试诚实:在写之前先命名会让测试失败的生产变更;断言真实行为,绝不针对 mock 行为断言;把仅测试用的代码留在测试工具中,不要放在生产类里;在 mock 依赖之前先理解它的副作用。

常见辩解与真相

  • 「太简单不用测」:简单代码也会坏,测试只需 30 秒。
  • 「我稍后再测」:事后写的测试立即通过,什么都证明不了。
  • 「事后测试达到同样目标」:事后测试回答「它做什么」,事前测试回答「它应该做什么」。
  • 「已经手动测过」:手动测试是临时的,没有覆盖记录,代码变更后无法重跑。
  • 「删除 X 小时很浪费」:沉没成本谬误,真正选择是用 TDD 重写(高信心)还是保留再加测试(低信心)。
  • 「留作参考,先写测试」:你会改编它,那就是事后测试。删除就是删除。
  • 「需要先探索」:可以,扔掉探索成果,从 TDD 开始。
  • 「测试难 = 设计不清」:听测试的,难测就是难用。
  • 「TDD 会拖慢我」:TDD 才是务实路径,能在提交前抓住 bug、防止回归、让你无畏重构。
  • 「手动测试更快」:手动不能证明边界情况,每次变更都要重测。
  • 「现有代码没有测试」:你在改进它,为现有代码添加测试。

红旗信号——停下并重新开始 测试之前写代码、实现之后才测试、测试立即通过、无法解释测试为何失败、测试是「稍后」添加的、为「就这一次」找借口、「我已经手动测过」、「事后测试达到同样目的」、「重在精神而非仪式」、「留作参考」或「改编现有代码」、「已经花了 X 小时,删除浪费」、「TDD 是教条,我很务实」、「这次不同因为……」。所有这些都意味着:删除代码,从 TDD 重新开始。

缺陷修复示例 缺陷:空邮箱被接受。 RED:写 test('rejects empty email') 断言提交空邮箱时返回错误 'Email required'。 验证 RED:运行 npm test,看到 FAIL: expected 'Email required', got undefined。 GREEN:在 submitForm 中检查 if (!data.email?.trim()) 返回 { error: 'Email required' }。 验证 GREEN:运行 npm test,看到 PASS。 REFACTOR:如需要,为多个字段提取验证逻辑。

验证清单 标记工作完成前:每个新函数/方法都有测试;在实现前亲眼看到每个测试失败;每个测试因预期原因失败(功能缺失,不是拼写错误);写最小代码让每个测试通过;所有测试通过;输出干净(无错误、警告);测试使用真实代码(mock 仅不可避免时);覆盖边界情况和错误。不能勾选所有项?你跳过了 TDD,重新开始。

卡住时的应对

  • 不知道怎么测:写期望的 API,先写断言,问你的人类伙伴。
  • 测试太复杂:设计太复杂,简化接口。
  • 必须 mock 一切:代码太耦合,使用依赖注入。
  • 测试设置庞大:提取辅助函数;仍然复杂就简化设计。

调试集成 发现 bug?写一个复现它的失败测试,遵循 TDD 循环。测试证明修复并防止回归。永远不要在没有测试的情况下修复 bug。

最终规则 生产代码 → 测试存在且先失败。否则 → 不是 TDD。没有你的人类伙伴许可,没有例外。