关于 tdd
指导开发者开展以行为为核心的测试驱动开发技能,涵盖测试边界协商和红绿循环。它强调经得起重构的测试,并规定测试夹具、运行器和边界情况的检查方式。
主要功能
- 01协商公开测试边界
- 02编写行为导向测试
- 03遵循红绿循环
- 04避免耦合实现的测试
- 05核验夹具与测试运行
适用场景
- 01为已报告缺陷设计测试
- 02以测试驱动逐步添加行为
- 03检查测试是否覆盖目标情况
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ hexsleeves/ skills --skill tdd安装后在 Claude Code 中输入/tdd确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEmocking.mdtests.mdagents/openai.yaml详细介绍
测试驱动开发
TDD 是红 → 绿循环。本技能为这个循环提供参考,确保产出值得保留的测试:什么是好的测试、测试应放在哪里、反模式,以及循环的规则。每个部分都适用于每一轮循环:在循环之前和过程中查阅,而不是结束之后。
探索代码库时,请阅读 GLOSSARY.md(如果存在),以确保测试名称和接口用语符合项目的领域语言,并遵循你所修改区域的 ADR。
什么是好的测试
测试通过公共接口验证行为,而不是实现细节。代码可以彻底改变,测试则不应如此。好的测试读起来像一份规范:“用户可以使用有效购物车结账”能准确说明存在什么功能,而且不会受重构影响,因为它不关心内部结构。
对于游戏或模拟规则,规则测试要证明规则正确,而不是证明平衡性。至少编写一个测试,通过生产环境中的移动或交互模型运行规则,并采用退化策略(什么也不做、守在最佳位置、机械地扫过、只有一个活跃 Player,其余都闲置),并断言这些策略的得分明显低于专注的策略。测试驱动程序从生产环境的辅助函数获取输入,而不是重新描述这些输入;它不能比真实玩家更厉害:它会移动,并且不会在一个 tick 中到处行动。
示例请参阅 tests.md,模拟指南请参阅 mocking.md。
接缝:测试应放在哪里
接缝是你进行测试的公共边界:你可以在此观察行为,而不必深入内部。测试应位于接缝处,绝不能针对内部实现。
只在事先约定的接缝处测试。 编写任何测试之前,先写下要测试的接缝并与用户确认。未经确认,不得在该接缝处编写测试。根据用户批准的计划,如果模块现有规范已经测试了某些接缝,这些接缝便视为已约定:在编写第一个测试之前,用一行说明这些接缝,然后继续。只有需要新增接缝,或模块没有规范时,才需要询问。你无法测试所有内容,因此提前约定接缝,才能让测试工作聚焦于关键路径和复杂逻辑,而不是每一种边缘情况。
说明接缝之前,请阅读仓库中记录的测试规则(CODING_STANDARDS.md、CONTRIBUTING.md,或任何 AGENTS.md 中指定的评审规则)。如果其中有规则为某类更改指定了接缝,例如“竞态修复需要 Postgres 测试”,那么该规则对相关部分的优先级高于现有规范默认值。
询问:“公共接口是什么,我们应该测试哪些接缝?” 为每个建议的接缝用一行说明它能发现什么问题,以及无法发现什么问题。
通过生产环境的封装层进行测试。 如果生产环境会封装一次调用(客户端设置、缓存、中间件),集成测试就应通过该封装层运行,或使用相同设置运行,并在测试中指出该封装层。跳过封装层的接缝证明的是另一条路径;例如,已部署客户端启用了查询缓存,那么就必须在应用该缓存设置的情况下测试查询。
进程信号和终端状态需要在注入的接缝之外进行一次检查。伪造的事件发送器或流无法显示终端挂断、随后出现的 stdio 错误或默认退出行为。应针对构建好的 CLI 进行一次真实 PTY 运行(用 tmux kill-session 触发 SIGHUP,用 send-keys C-c 触发 SIGINT);如果时间窗口太短无法触发,则添加一个模拟该处理程序的最简脚本。
对于重叠的定时器,需要使用能让定时器保持挂起的时钟。当行为涉及两个或多个同时等待的定时器或重试(去重、合并、取消、加入现有等待)时,伪造的 sleep 应保留其解析函数,直到测试将其释放。会立即推进并解析的时钟会将定时器串行化,无法展现重叠情况。在红色步骤中测试却通过了,这是一个信号:应更改接缝,而不是断言。
反模式
- 与实现耦合:模拟内部协作者、测试私有方法,或通过旁路进行验证(查询数据库,而不是使用接口)。判断依据:重构后测试会失败,但行为并未改变。
- 同义反复:断言以与代码相同的方式重新计算预期值(
expect(add(a, b)).toBe(a + b)、用相同方式手工推导的快照、断言常量等于自身),因此测试按构造就会通过,永远不可能与代码结果不一致。预期值必须来自独立的真实依据:已知正确的字面值、完整示例或规范。 - 横向切片:先编写所有测试,再编写所有实现。批量测试验证的是_臆想_的行为:测试的是事物的_形状_,而不是面向用户的行为;实际发生变化时,测试可能无法察觉;并且你会在理解实现之前就确定测试结构。应改用纵向切片:一次编写一个测试、完成一个实现,然后重复;每个测试都是一枚曳光弹,根据上一个循环的经验调整方向。
循环规则
- 先红后绿。 先编写会失败的测试,然后只编写足以让测试通过的代码。不要预想未来的测试,也不要添加推测性功能。
- 红要有计数。 查看测试运行器实际运行的测试数,而不只是失败数:红表示至少有一个测试已执行,并因预期原因失败。运行了 0 个测试,说明测试工具链出了问题。会对参数进行 glob 匹配的运行器(
node --test等)在路径包含[...]片段时(例如 Next.js 动态路由)可能匹配不到任何内容,即使 shell 命令对路径加了引号也一样;请传入能匹配该文件的 glob,并确认测试计数。 - 已固定的行为通过变异测试变红。 固定代码已有行为的测试(特征测试、调用预算、回归固定项)首次运行就会通过,而缺少辅助函数导致的编译错误不算红。首次运行前,先根据代码或规范推导预期值,然后对被测代码做最小的临时修改,确保测试能捕获该修改,观察测试失败,再还原代码。请在 seam 提案中写明这一步,以便用户事先同意。如果变异后测试仍通过,说明测试夹具有误:检查断言时测试依赖的状态是否仍然存在(未合并的部分、冷缓存、排序,或按查询文本匹配的故障钩子),因为存储引擎或运行时可能已提前将其消除。
- 测试夹具必须能到达目标情况。如果测试在到达被测情况前就已饱和(遇到墙、上限、空队列或超时),那么测试通过的原因就不对;因此要断言测试确实到达了目标(行进距离超过限值)。断言守卫保护的属性,而不是系统会自行修复的最终状态。测试某项行为是否会被拒绝或阻止时,要进行一次直接移除阻挡因素的变异;读取绿色结果前,要对移除守卫的变异体重新运行一次新的模糊测试;此外,每个变异步骤都要先断言文件确实已更改,再运行测试并计数。
- 更改容差后,重新运行其记录的测试用例。如果更改缩小或替换了容差、超时、重试次数或阈值,请查找设定旧值的提交和 PR(
git log -S <CONSTANT>),重新运行其中记录的压力测试或测试用例,并在相同条件下与 main 进行交替运行对比。默认速度下测试通过,无法说明为慢速路径设置的常量是否合理。 - 每次运行值都需要三次通过。若新测试的夹具包含每次运行都会变化的值(提交 SHA、时间戳、随机 ID、临时路径),则至少连续三次运行通过后,才能计为通过;由这些值构建的期望集合,也必须像实际集合一样进行规范化处理(两边都排序,或按集合比较)。
- 通用名称要覆盖可达域。名称声称某属性适用于所有输入的测试(例如“每个”“从不”“总是”“两者都不可能”),必须覆盖生产代码实际能达到的输入域;应根据生成输入的代码路径推导该域,而不是只看功能首次使用的值。如果该域过大,无法完整遍历,名称就要说明测试抽样范围。
- 带种子的模糊测试要统计不同用例数。在 JavaScript 中,要使用整数安全的伪随机数生成器(基于
Math.imul,例如 mulberry32,或使用库),绝不要使用基于 double 的教科书式 LCG,其乘积会超过 2^53,导致很快进入短周期。将失败数视为比例或比较两组计数之前,先统计生成用例中的不同用例数(new Set(cases.map(JSON.stringify)).size),并将其与运行次数一同报告。 - 传播需要哨兵值。 测试配置值或环境变量是否传递到了各个使用方时,夹具值必须与代码中的所有默认值和回退值不同(使用
{"test":"key"},而不是{}),并且测试要断言每个使用方都收到了该值。夹具值若与默认值相同,那么无论该值是被传递还是被丢弃,测试都会通过。 - 没有运行器,就不写生产代码。 如果测试运行器无法访问(远程机器离线、依赖项无法安装),就不要再为该行为编写生产代码。先恢复运行器并亲眼确认测试变红,否则就停止并向用户报告。
- 一次只处理一个切片。 每个周期只处理一个 seam、一个测试和一个最小实现。
- 重构不属于这个循环。 重构应放在评审阶段(参见
code-review技能),而非红 → 绿的实现循环。