测试驱动开发(Test-Driven Development)
TDD 就是 red → green(红 → 绿)循环。本技能是让这一循环产出值得保留的测试的参考:什么样的测试是好测试、测试放在哪里、有哪些反模式,以及循环的规则。每个小节在每一轮循环中都适用——在循环开始前和进行中查阅它们,而不是事后才看。
在探索代码库时,阅读 CONTEXT.md(如果存在),让测试名称和接口词汇与项目的领域语言保持一致,并尊重你所触及区域的 ADR。
什么是好的测试
测试通过公共接口验证行为,而不是实现细节。代码可以彻底改变;测试不应随之改变。好的测试读起来就像一份规格说明——"用户可以用有效的购物车完成结账"准确告诉你存在什么能力——而且它不在乎内部结构,所以能扛住重构。
示例参见 tests.md,mock 指南参见 mocking.md。
Seam(接缝)——测试放在哪里
seam(接缝) 是你进行测试的公共边界:在不深入内部的情况下观察行为的接口。测试放在 seams 上,绝不针对内部实现。
只在你预先约定的 seams 上测试。 在编写任何测试之前,写下将要测试的 seams 并与用户确认。未经确认的 seam 上不写任何测试。你不可能测试所有东西——提前约定 seams,才能让测试精力落在关键路径和复杂逻辑上,而不是撒在每个边界情况上。
询问:"公共接口是什么,我们应该测试哪些 seam?"
当接口本身的形态存疑时——模块该有多深、seam 该放在哪里、接口应暴露什么——调用 skill tool,使用 "codebase-design" 获取相关词汇。它是 module、interface、depth、seam、adapter、leverage 和 locality 这些术语的共同来源,是一份供查阅的参考,而不是要运行的一次会话。
反模式
- Implementation-coupled(实现耦合)——mock 内部协作者、测试私有方法,或通过旁路通道验证(直接查询数据库而不是使用接口)。典型特征:重构时测试破裂,但行为并没有变化。
- Tautological(同义反复)——断言用与代码相同的方式重新计算期望值(
expect(add(a, b)).toBe(a + b)、以同样方式手工推导出的 snapshot、断言常量等于自身),因此它凭构造就能通过,永远不可能与代码产生分歧。期望值必须来自独立的真值来源——已知正确的字面量、手算示例、spec。 - Horizontal slicing(水平切片)——先写完所有测试,再写全部实现。批量测试验证的是_想象中的_行为:你测试的是事物的_形状_而不是面向用户的行为,测试对真实变化不敏感,而且你在理解实现之前就锁定了测试结构。改为按 vertical slices(垂直切片) 工作——一个测试 → 一个实现 → 重复,每个测试都是一颗 tracer bullet(曳光弹),对上一轮循环教给你的东西做出回应。
循环的规则
- Red before green(先红后绿)。 先写失败的测试,然后只写足以让它通过的代码。不要预想未来的测试,也不要添加投机性的功能。
- One slice at a time(一次一个切片)。 每轮循环一个 seam、一个测试、一个最小实现。
- Refactoring is not part of the loop(重构不属于循环)。 它属于 review 阶段(参见
code-review技能),而不是 red → green 实现循环。