关于 tdd
tdd 是面向编码 Agent 的测试驱动开发技能,要求先编写并验证失败测试,再逐步实现和重构。它强调在约定的测试边界上验证行为,并为测试关联需求 ID。
主要功能
- 01先写失败测试再实现
- 02逐步执行红绿重构循环
- 03要求确认测试先按预期失败
- 04在约定的公共边界编写测试
- 05为测试标注需求 ID
- 06检查测试替身与完成条件
适用场景
- 01为新行为建立测试优先的实现流程
- 02修复缺陷前编写可复现问题的测试
- 03按需求追踪测试覆盖与边界
- 04完成改动前检查测试验证条件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jayden-dang/ skills --skill tdd安装后在 Claude Code 中输入/tdd确认已加载。
详细介绍
TDD
铁律
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
先写了实现再写测试?删掉它。不是“留着当参考”,也不是“等测试补上时再调整”——你最终会测试自己构建的东西,而不是需求所要求的内容。删除就是删除;从测试开始重新实现。违反这条铁律的字面要求,就是违背它的精神。
只有得到用户明确许可时才存在例外:一次性原型、生成的代码、纯配置。“这次就跳过吧”不算例外;这正是这项技能要阻止的自我辩解。
测试应放在哪里:预先约定的接缝
接缝是一个公共接口,无需深入内部即可观察行为。测试只能放在功能的 design.md 中 Seams for testing 所列出的接缝上。如果表格未确认某个接缝,就拒绝在该接缝上编写测试。此项工作没有 design.md(第 0/1 级变更、遗留区域)?先向用户提出接缝方案并取得同意,再编写第一个测试。测试投入是一项预算——约定好的接缝能让投入聚焦于关键路径,而不是每个内部细节。
循环
一次只处理一个垂直切片:一种行为 → 一个测试 → 一个最小实现 → 重复。每个循环都要根据上一个循环的发现进行调整。不要先一次性写完所有测试,再写完所有代码——批量编写的测试验证的是想象出来的行为。
RED——编写一个失败的测试。 一个行为,一个能说明该行为的名称,在约定的接缝上针对真实代码编写断言。标记该测试所验证的需求 ID(见下文)。修复 Bug?必需的子技能:先使用 debug 查明根本原因——RED 测试必须复现该 Bug,并且在修复前因该 Bug 而失败。
验证 RED——必须执行,绝不跳过。 运行测试(使用 docs/agents/project.md 中的单文件命令)。确认测试确实失败、失败消息符合预期,并且失败原因是缺少该行为——而不是拼写错误或导入错误。测试立即通过? 你测试的是已存在的行为——修改测试,而不是代码。测试报错而非失败?修复错误并重新运行,直到测试正确失败。
GREEN——编写能通过测试的最简单代码。 不添加推测性的参数、不增加额外功能、不触碰无关代码。
验证 GREEN——必须执行。 运行完整测试套件。确认:新测试通过、其他测试仍全部通过,并且输出干净无误——零警告、零无关错误,也没有任何“打算之后清理”的内容。新测试失败?修复代码,绝不要放宽测试。其他测试失败?立即修复,然后再做其他事。
REFACTOR——仅在测试全绿时进行。 消除重复、改进命名、提取辅助函数。每次修改后都重新运行测试套件。不添加任何行为。
然后处理下一个切片。
需求标记
每个测试都要标记其所验证的需求 ID,并遵循 docs/agents/project.md 中的约定(Test annotation conventions 表)常见形式如下:
| 层级 | 约定 |
|---|---|
| E2E(例如 Playwright) | { tag: ['@CODE-N.M'] } |
| 单元测试(例如 Vitest) | annotate('CODE-N.M', 'requirement') 或将 ID 写入测试名称 |
| 编译型语言(例如 Rust) | 在测试上方添加 /// REQ: CODE-N.M 文档注释 |
没有需求覆盖该行为?对于第 ≥1 级的工作,这是一个缺口——请向用户指出,而不是交付无法追踪的测试。如果缺少 docs/agents/project.md,请说明这一点,建议运行 setup-repo,并退而将 ID 写入测试名称。
反模式及其应对
| 反模式 | 表现 | 应对 |
|---|---|---|
| 同义反复的测试 | 预期值用与代码相同的方式重新计算(expect(sum(items)).toBe(items.reduce(...))),因此测试必然通过 | 预期值来自独立的事实来源:已知正确的字面值、完整示例或需求文本 |
| 与实现耦合的测试 | 重构后即使行为未变,测试也会失败;断言调用次数或顺序、私有方法,或通过旁路验证(例如直接查询原始数据库,而不是使用接口) | 仅通过公共接口断言可观察到的行为 |
| 测试模拟行为 | 断言检查模拟对象是否存在或是否被调用;移除模拟对象后测试就会失败 | 测试真实组件,或者完全停止对模拟对象进行断言 |
| 仅供测试使用的生产类方法 | 某个方法(例如 destroy())只在测试文件中调用 | 将其移至测试工具;生产类只保留生产 API |
| 不完整的模拟对象 | 模拟对象只包含当前测试恰好会读取的字段;集成时遗漏的字段会导致问题 | 复刻完整的真实数据结构——包含真实响应中的每个字段 |
| 模拟自有模块 | 为了“隔离”而替换内部协作者 | 只在系统边界处模拟(外部 API、时间、随机性,有时也包括文件系统或数据库)。绝不要模拟自己编写的代码——如果觉得有必要这么做,说明设计耦合过紧:应改为注入依赖 |
如果模拟对象的设置代码比测试逻辑还长,或者移除模拟对象后测试就会失败,说明问题出在模拟对象上——优先通过真实组件编写集成测试。
自我辩解
| 想法 | 事实 |
|---|---|
| “太简单了,不值得测试” | 简单的代码也会出错;写测试只需一分钟 |
| “我之后马上补测试” | 事后编写的测试一运行就通过,什么也证明不了——你从未见过它捕获任何问题 |
| “事后测试本质上效果一样” | 事后测试回答“这段代码做什么?”,先写测试回答“这段代码应该做什么?”——前者会受到你的实现影响 |
| “我已经手动验证过了” | 临时执行、没有记录、无法重复。下次改动时,这些验证就不复存在了 |
| “删掉几个小时的工作太浪费了” | 沉没成本。保留未经测试的代码才是真正的浪费 |
| “写测试时先留着,作为参考” | 这还是事后测试,只是多了几个步骤 |
| “这个测试很难写” | 难以测试就意味着难以使用——简化接口,而不是跳过测试 |
| “TDD 会拖慢我” | 比打字慢,比调试快 |
| “这个文件本来就没有测试” | 你正在改它;你的改动就应该有测试 |
危险信号——停下来重新开始
- 实现已经存在,但对应的测试不存在
- 新测试第一次运行就通过了
- 你说不清测试在 RED 阶段为何失败
- 测试针对的接口不在设计的接口表中
- 测试没有需求 ID
- “就这一次”“先留着参考”“重在精神,不拘形式”“这次情况不同”
出现以上任一情况:删除未经测试的代码,回到 RED。
宣布完成前
- 每个新行为都有一个你亲眼看着先失败、且因预期原因失败的测试——错误路径和边界值也算行为
- 完整测试套件全部通过,输出干净(零警告、零错误)
- 每项测试都按照
docs/agents/project.md标注了对应的需求 ID - 所有测试都位于
design.md中约定的接口处(或与用户约定的接口处) - 仅在系统边界处使用模拟对象,数据结构完整,不对模拟对象进行断言
有任何一项无法勾选?工作就还没完成。必需的子技能:在声称完成前使用 verify。