关于 tdd
tdd 是一项将测试驱动开发用于新功能、修复、重构和行为变更的技能。它要求先确认测试因预期原因失败,再编写最少实现使其通过,并在不增加新行为的前提下重构。
主要功能
- 01先于生产代码编写行为测试
- 02确认测试因预期原因失败
- 03编写通过测试所需的最少代码
- 04实现后运行相关测试
- 05仅在测试保持通过时重构
适用场景
- 01通过测试优先的循环开发新功能
- 02为错误修复添加回归测试
- 03在保留测试覆盖的同时重构行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gusen1453/ creed --skill tdd安装后在 Claude Code 中输入/tdd确认已加载。
详细介绍
TDD
输出语言: 使用用户自己的语言回答并编写产物(匹配用户的消息,而不是仓库的历史)。
概述
先编写测试。观察它失败。编写通过测试所需的最少代码。重构。
核心原则:如果你没有观察到测试失败,就不知道它是否测试了正确的内容。
必须搭配使用: 使用 test-design 决定要提供什么输入、断言什么以及模拟什么。此技能只规定节奏。形式主义的 RED 测试仍然不允许——失败的戏码测试证明不了任何事。
适用场景
始终适用: 新功能、错误修复、重构、行为变更。
例外情况(先询问用户): 一次性原型、生成的代码、没有行为的纯配置。
心里想「这次就跳过 TDD 吧」?停下——这只是找借口。
铁律(不可妥协)
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
先写了代码再写测试?删掉代码。从头开始。
没有例外:
- 不要把它留作「参考」
- 不要在编写测试时对它进行「改造」
- 不要查看它
- 删除就是彻底删除
违反字面要求就是违背其精神。
工作流(调用时)
宣布「正在使用 tdd 来……」,然后每个循环步骤创建一个待办事项:
- RED — 编写一个失败的行为测试(应用 test-design:契约、精简断言、手写
expected) - 验证 RED — 运行测试;确认它因正确原因失败(缺少功能,而不是拼写错误/错误)
- GREEN — 编写通过测试所需的最少生产代码
- 验证 GREEN — 运行测试;所有相关测试均通过;输出无误
- 重构 — 清理代码;保持通过;不添加新行为
- 重复 — 下一个行为 → 下一个 RED
之后发现错误?先编写能复现错误的失败测试,然后执行相同的循环。
Red → Green → Refactor
RED — 编写一个失败的测试
一个行为。名称清晰(角色 + 场景 + 预期)。使用真实协作者,除非 IO 迫使你使用 Fake/Recording(参见 test-design)。
test("retries failed operations 3 times", async () => {
let attempts = 0
const operation = () => {
attempts++
if (attempts < 3) throw new Error("fail")
return "success"
}
expect(await retryOperation(operation)).toBe("success")
expect(attempts).toBe(3)
})
不好的做法:名称含糊、只断言模拟调用次数,或使用生产逻辑计算 expected。
验证 RED — 必须执行
运行目标测试。确认:
- 测试失败(而不是因拼写错误/导入问题报错)
- 失败信息与缺少的行为相符
- 如果测试立即通过 → 你测试的是现有行为,或没有做任何有意义的断言——修正测试
- 如果测试报错 → 修复测试框架,然后重新运行,直到测试正确地失败
GREEN — 最少代码
编写能通过测试的最简单代码。不添加额外功能,不顺手重构,不添加 YAGNI 式选项对象。
验证 GREEN — 必须执行
再次运行。所有相关测试均通过。不忽略新的警告/错误。
重构
只在测试通过后进行:调整名称、消除重复、提取辅助函数。保持测试通过。不添加行为(新行为 → 新 RED)。
好测试与坏测试(从节奏角度看)
| 好 | 坏 | |
|---|---|---|
| 规模 | 一个行为;名称中有「和」→ 拆分 | 包罗万象的测试 |
| 名称 | 描述可观察到的行为 | test1 / works |
| 证明 | 你观察到测试因正确原因进入 RED | 首次运行就通过 |
| 深度 | 断言契约(test-design) | 只有 toHaveBeenCalled / 用覆盖率充数 |
合理化借口对照表
| 借口 | 事实 |
|---|---|
| “我之后会测试验证” | 测试立即通过什么也证明不了。你根本没看到它捕获这个 Bug。 |
| “之后测试也能达到相同目标” | 之后 =“这段代码做什么?”首先 =“这段代码应该做什么?” |
| “已经手动测试过了” | 临时测试,没有记录,也无法重跑。 |
| “太简单,不值得测试” | 简单的代码也会出错。编写一个有针对性的测试成本很低。 |
| “删除 X 小时的工作很浪费” | 沉没成本。未经验证的代码就是债务。删除并重新开始。 |
| “先留着作参考/测试时改编” | 那就是先实现再测试。删除就是真的删除。 |
| “得先探索一番” | 先探索,然后丢弃探索代码;从头开始,干净地执行 TDD。 |
| “TDD 会拖慢我” | 在生产环境中调试更慢。务实的做法 = 测试优先。 |
| “很难测试” | 设计异味。简化接口(test-design § hard-to-test)。 |
| “现有代码没有测试” | 你正在修改它——为你涉及的行为添加失败测试。 |
| “就这一次/重精神不重仪式” | 字面要求就是精神要求。未经用户同意,不得例外。 |
危险信号——删除代码并重新开始
- 在失败测试之前编写生产代码
- 实现之后才编写测试
- 测试首次运行就通过
- 无法解释 RED 为什么失败
- 使用
--no-verify/“为了节省时间”跳过测试运行 - 将测试前的代码留作“参考”
- “务实的捷径”/“这次情况不同,因为……”
任意一项出现 → 删除生产代码。从 RED 重新开始。
反模式(简要)
- 测试模拟对象的行为,而不是单元本身
- 在生产类型上添加仅供测试使用的方法
- 模拟一堆相邻组件(通常说明代码只是胶水 → 优先选择集成测试,或重新设计——test-design §1/§5)
检查清单(每种行为)
- 值得编写真正的测试吗?(test-design ROI)——否则别在胶水代码上假装使用 TDD
- 根据契约编写 RED;观察到它因正确原因失败
- GREEN 保持最精简;验证通过
- REFACTOR 阶段保持绿色;没有偷偷加入新行为
- 只为外部环境使用模拟;断言范围精确且难以满足
交接
转换关卡——当一个功能切片完成时(或离开 tdd 之前),询问用户(使用一次 AskUserQuestion;绝不自动继续):
A) Recommended: Proceed → review (then commit-and-push)
B) Review first — run **review** on the code slice (`references/code.md`; upstream = plan tasks / spec), then return to this gate
C) Adjust — back into a tdd RED→GREEN cycle for the issue, then return to this gate
D) Something else (I will type it)
- 测试用例质量不明确 → test-design
- 边界/模拟堆积 → solid
- 功能形态不明确 → grill → 先执行 write-spec,再进行 TDD
- 准备发布 → review,然后 commit-and-push