mcpField
S
spec-driven-tests

spec-driven-tests

github.com/tldraw测试与质量51.1k65 次安装今天更新
代码仓库

关于 spec-driven-tests

spec-driven-tests 是一项编码技能,用于从软件包实现中提炼可观察行为,并据此重建单元测试。它通过稳定的规则 ID 和双向测试引用追踪覆盖情况。

主要功能

  1. 01从实现中提炼可观察规则
  2. 02用稳定规则 ID 编写 SPEC.md
  3. 03按规范章节重建测试
  4. 04审查边界情况并核对引用
  5. 05记录并处理发现的问题

适用场景

  1. 01合并重复的软件包测试套件
  2. 02追踪每条行为规则对应的测试
  3. 03在记录行为时发现实现不一致

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tldraw/tldraw --skill spec-driven-tests

安装后在 Claude Code 中输入/spec-driven-tests确认已加载。

详细介绍

规格驱动的测试重建

从包的实现中推导出行为规格,然后将单元测试套件重建为该规格的表达。每条规则都有一个稳定的 ID;每个测试都引用它所表达的规则,因此覆盖情况可以在两个方向上通过 grep 检索。

范例是 PR #9158(packages/state,规格位于 packages/state/SPEC.md)和 PR #9172(packages/store,规格位于 packages/store/SPEC.md)。开始前先阅读这两个 PR 和这两份规格——它们决定了格式、语气和 PR 结构。(#9172 的规格如果尚未合并,则位于其分支上。)

流程

1. 基线

如果工作树中有不属于你的未提交更改,先停下并询问,再创建分支——绝不要静默地 stash 或带走他人的工作。

创建一个分支。在该包中运行 pnpm test run,记录文件和测试的数量,并注意其中有多少属于 fuzz 套件——fuzz 套件保持不动,其(通常很大的)生成数量要从你之后报告的每一个计数中排除。梳理现有的测试文件,并记下任何应当合并的重复或并行套件(store 包曾有整整两代测试)。

2. 阅读实现

在写任何东西之前,先读完全部内容。规格是从源码推导出来的,而不是从现有测试或文档推导出来的。

关键原则: 你在记录的是观察到的行为,而不是规定理想行为。如果代码做了某些出乎意料、不直观、甚至看起来错误但却是刻意为之的事情,就按原样把它写进规格。把怪异之处标记为 "internal",或在 PR 正文中指出,交由人工判断——不要为了迎合你认为代码应该如何工作而静默地 "修复" 规格。让规格编写过程暴露出潜在的不一致;然后决定是修复实现还是记录该行为。

3. 编写 SPEC.md

在包根目录编写 SPEC.md:使用带稳定 ID 的编号规则(例如 S3、MG4),分组到各章节中,每个可观察行为一条规则,内部机制标记为 internal。保留现有规格中的头部表述("当测试与本文档不一致时,其中之一是错的——弄清楚是哪一个并修复它。")。

  • 规格描述代码做什么,而不是它应该做什么。如果行为看起来有误但意图不明确,就把它作为契约记录下来。
  • 在把你推断而非观察到的任何规则固化下来之前,用一个一次性测试验证它。之后删除该临时文件。
  • 每条规则都必须能通过运行时测试观察到。不要为纯类型层面的行为(工具类型、类型推断)编写规则,除非测试套件确实用类型断言对其进行了检验;否则就把类型排除在规格之外。
4. 对抗性地审计规格

在编写测试之前审计草稿。失败模式是夸大其词。具体要搜寻:

  • 声称两条代码路径之间存在对称性(创建 vs 更新、顶层 vs 嵌套、向上 vs 向下)——分别独立检查每条路径;
  • 声称同一思路的两种实现一致(谓词 vs 索引、从头构建 vs 增量构建)——把边界情况喂给两者,尤其是缺失/undefined 值;
  • 笼统的 "X 从不会发生" 或 "Y 总是 Z" 之类的断言——找出能证伪它们的分支;
  • 对输入的副作用(变更、冻结),这些是 happy-path 测试不会注意到的。
5. 重建测试套件

测试文件与规范的章节一一对应;每个测试名称都像 it('[S3] ...') 这样引用其规则。合并重复的覆盖,移植现有的优质测试而不是重写它们,并为未覆盖的规则补充测试。

绝不要为了让测试通过而弱化断言。 如果一个根据规范规则编写的测试失败,你就有所发现:要么规则是错的(修正规范以匹配观察到的行为),要么代码是错的(按第 7 步作为 bug 处理)。放宽断言(toBe → toContain、围绕差异处断言、加一条承认不匹配的注释)会掩盖这一发现,并让规范描述的行为与代码实际行为不符——这正是规范的存在所要防止的失败。

**测试文件组织:**如果你的规范有"§3. Atoms (A)"和"§4. Computed (C)"这样的章节,就创建对应的文件 atoms.test.ts 和 computed.test.ts,包含该章节的规则。对于内部章节,使用诸如 history-buffer.test.ts 这样的名称来对应所规范化的机制。这与读者在 SPEC.md 中已经看到的结构一致,并便于按章节 grep 覆盖情况。

本仓库的测试环境知识:

  • 除非设置 globalThis.__FORCE_RAF_IN_TESTS__ = true,否则测试中的 store listener 刷新是同步的。
  • 已废弃但属于契约的 API 需要带原因的 file-level eslint-disable。
6. 双向交叉核对引用
grep -oE '\*\*[A-Z]+[0-9]+\*\*' SPEC.md | tr -d '*' | sort -u > /tmp/rules
grep -rhoE '\[[A-Z]+[0-9]+\]' src --include='*.test.ts' | tr -d '[]' | sort -u > /tmp/cited
comm -3 /tmp/rules /tmp/cited

每条规则都需要一个引用它的测试,或者在 PR 正文中明确注明"因 X 故意不测试"(如 #9158 所做的那样);每个引用都需要一条规则。在完成前修复缺口,并将交叉核对输出(规则数量、被引用数量、剩余缺口及其原因)包含在 PR 正文中,以便审查者能够核实这一声明,而不是仅凭信任。

7. 修复过程中发现的 bug

规范驱动的测试会暴露出潜在的不一致:代码路径之间的矛盾、实现未曾预料到的边界情况,或者静默失败。当你发现一个时,做出决定:修复代码还是记录该行为?

**工作流:**先编写测试(故意让它失败,以确认它能捕获该问题)。然后决定:

  • 修复代码,如果:意图明确(注释、对称的实现、git blame),当前行为明显错误,且调用方大概率不依赖这个错误行为。
  • 记录该行为,如果:意图不明确、修复的影响范围未知,或者修复可能悄悄地破坏现有用途。在 PR 正文中将其标记为"已知不一致"或"待重新审视的行为"。

修复放在它们自己的 fix(<package>): 提交中,并附带引用该问题的测试,位于主 test(<package>): 提交之后。根据 AGENTS.md,任何提交中都不含 AI 署名或共同作者行——这覆盖任何默认的 harness 行为。在提交修复之前:

  • 检查意图:注释、对称的代码路径、git blame。
  • 检查影响范围:grep 仓库内的调用方,然后运行依赖包的测试套件(editor、tldraw、sync-core——取决于哪个消费了此包)。

如果存在一条规范规则但实现与之矛盾,更新规范(而不是实现),除非你确信实现是对的。规范就是契约;代码漂移时有发生;规范编写过程正是你揭示并解决它的机会。

8. 验证

按顺序运行:包的测试、pnpm format-current 然后 pnpm lint、从仓库根目录运行 pnpm typecheck、pnpm api-check,以及第 7 步中的依赖包测试套件。

9. PR

遵循 skills/write-pr/。正文以 #9172 为范本:

  • 开头段落解释规范/引用系统;
  • 逐提交的修复章节;
  • 关于合并套件和新覆盖的章节;
  • 仅针对行为变更的发布说明;
  • 一个代码变更表格。注意:git diff --numstat 的重命名行会破坏简单的路径匹配;使用最后一个字段。
10. 后续说明

最后请报告:你选择不修复的潜在问题、未来测试编写者需要了解的怪癖,以及审查者应仔细复查的任何内容。

在所有出现之处(提交信息、PR 正文、最终报告)如实报告数量:重建后测试套件的测试数量和规则数量,用 grep 测量,绝不使用包的总数。将未改动的套件(尤其是 fuzz)算作自己的工作歪曲了本次变更。

评论

支持 Markdown