测试即对照样本
概述
测试就是一个对照样本:它固定了一个预期,使得任何行为变化都变得可见。纪律是不要修改测试去匹配代码;要理解它们为何不一致。
规则
-
行为变更:测试先行。 编写会失败的测试,观察它因正确的原因失败,然后编写能通过的最少量代码。如果你没有亲眼看到它失败,你就不知道它测试的是你认为的东西。这个循环本身属于
superpowers:test-driven-development;本技能规定了测试必须包含什么,以及一旦测试存在,失败意味着什么。无论是否安装了该技能,此规则均适用。 -
结构性变更由未修改的整套测试来验证。 纯粹的结构性变更——重命名、移动、拆分、提取,且无观察到的行为变化——不需要新的测试。其门槛恰恰相反:整套现有测试无需编辑任何测试即可通过。 这就是为什么测试先行在这里不适用——因为没有需要指定的行为,所以没有测试能先有意义地失败。 如果一项结构性变更迫使你修改测试,那它就并非结构性变更。停下来,用下一条规则重新分类。
-
跨越合同边界的重命名不是重构。 关键问题不在于“代码是否改变了行为?”,而在于“这个名称是否对你无法控制的事物可见?”
重命名对象 分类 门槛 内部符号、私有辅助函数、本地文件 结构性 现有整套测试无需修改即可通过 导出函数、API/GraphQL 字段、路由、CLI 标志、数据库列、事件名称、配置键 行为性——它破坏了调用方 测试先行 + 添加并弃用 即使对同一数据进行相同操作,公开重命名仍然是破坏性变更,因为名称本身就是合同(海勒姆定律——参见
performance-and-scale)。将其视为行为性变更:通过测试先行周期添加新名称,在弃用窗口期保留旧名称及其通过的测试,只有在调用方迁移完成后才移除旧名称。第三种情况是测试以字符串形式引用名称——快照、DI 容器键、反射、fixture 路径。这些在内部重命名时会失败。这不是规则失效;而是表明测试与实现细节耦合了。修复耦合,而不是重新基线化。
-
重构不得修改测试;需求变更才能修改。 不同的行为:
- 重构导致测试失败 → 测试捕获了非预期的行为变化。停下来理解它。 编辑测试以使重构通过,会破坏你的仪器。
- 需求确实发生了变化 → 有意识地更新测试,并作为独立可审查的变更记录下来。
- 禁止的行为是静默地重新基线化。
-
对称覆盖——每个规则都要双向测试。 证明授权执行者能够 并且 未授权执行者不能。仅测试快乐路径只能测试规则的一半。
-
权限/角色规则是一个矩阵: 每个有意义的(角色 × 动作 × 资源)单元格,都需测试允许和拒绝。新角色只有在其拒绝案例存在时才算完整。一个原本拒绝的测试突然通过了,这是安全回归——必须显式审查并接受,绝不能重新基线化。
-
入口点不得静默添加/移除。 路由、命令、端点各自都需要存在性及冒烟测试覆盖,以便添加/移除操作强制触发测试修改。
-
按规模测试: 小型(纯逻辑,无 I/O,常量),中型(真实数据库/进程,隔离),大型(全栈)。小型测试多,中型测试少,大型测试最少。警惕缺失的中间层——既非单元测试也非集成测试覆盖的服务器/应用逻辑,因为每一层都假设另一层会测试它。
-
不稳定测试一经发现立即隔离。 一个在未改代码的情况下随机通/不通过的测试,会破坏对整个测试套件的信任。立即隔离(移出阻塞门,提交修复方案)——永远不要容忍间歇性失败,也不要静默删除。小型测试必须是确定性的:没有真实时钟、随机性或网络。
-
测试代码要 DAMP,不要 DRY。 描述性且有意义的短语优先于去重。测试必须在隔离阅读时一目了然。对测试应用 DRY 是违规,而非美德(参见
change-hygiene-and-code-craft)。
常见的合理化借口
| 借口 | 现实 |
|---|---|
| “我先写代码,再补测试” | 那么测试就是为代码定制的,而不是为需求定制的。你从未看到它失败,所以你不知道它测试了什么东西。 |
| “只是重命名,不需要测试” | 正确——如果名称是内部的。如果是导出/公开的,名称就是合同,重命名属于行为性变更。 |
| “我重命名了它,不得不修复三个测试,这很正常” | 不正常。要么你跨越了合同边界,要么那些测试与实现细节耦合了。重新分类。 |
| “测试现在不对了,我就更新一下” | 仅当需求改变时才这样做。如果你是在重构,测试捕获了真实的变化——调查它。 |
| “我测试了快乐路径,这就够了” | 一个规则直到拒绝案例也被测试才算完整。 |
| “这个拒绝测试现在通过了,太好了,发布吧” | 一个被禁止的动作成功了是安全回归,不是进展。 |
| “测试不稳定,重新运行 CI 就行” | 不稳定性掩盖了真正的 bug,并训练所有人忽视失败。隔离并修复。 |
| “让我用共享辅助函数把这些测试 DRY 一下” | 测试要 DAMP。有助于现场清晰性的重复应当保留。 |
红旗——立即停止
- 在编写会失败的测试之前,为行为变更编写生产代码
- 结构性变更的 diff 也触及了测试文件
- 在原地重命名公开/导出的名称,而不是采用添加并弃用策略
- 在重构期间编辑测试以使其通过
- 新的权限没有拒绝案例的测试
- 在未理解变更原因的情况下,重新基线化快照/断言
- 包含真实的
sleep、Date.now()、随机数据或网络调用的测试 - 以牺牲可读性为代价,为去除重复而重构测试代码
完整理由:宪法第四条,捆绑于 engineering-constitution/references/engineering-constitution.md。关于 GraphQL/路由合同的具体细节,请参见 graphql-contract-testing。