前言¶
让编程 Agent 写测试,最常见的情况不是它不肯写,而是它写得太快、一次写太多。你说「加一个结账功能,顺便补测试」,它往往会先把整套测试骨架铺出来,再一次性把实现填进去。表面上看红绿都走了,实际上这些测试验证的是它想象中的接口形状,而不是用户真正能观察到的行为。内部一重构,测试就碎;断言跟实现用同一套计算,测试永远不会失败。
Matt Pocock 把这件事收成了一个 Agent Skill,名字就叫 tdd。它不负责替你排期、拆票、提交,只规定红绿循环里每一圈该怎么走:测什么、测在哪、一次只写一个测试、一次只写刚好能让这个测试通过的实现。仓库 mattpocock/skills 把这类实践叫「给真正工程师用的 Skill」,tdd 是其中被标成 model-invoked 的工程技能之一:你可以输入 /tdd,Agent 在「先写测试」「red-green-refactor」「集成测试」这类任务上也会自己去读它。
本文根据 GitHub 上的 SKILL.md、配套的 tests.md / mocking.md,以及作者在 aihero.dev 上的技能说明交叉整理,介绍这个 Skill 解决什么问题、怎么安装、怎么用。
这是什么¶
tdd 是一份给 Agent 看的 TDD 规程,不是一个测试框架,也不是一次把功能做完的工作流。官方定位很明确:它是 reference(规则手册),不是 driver(驱动程序)。真正跑循环的是你,或者同一仓库里的 implement Skill。
来源与归属:
- 作者:Matt Pocock(Total TypeScript / AI Hero)
- 仓库:https://github.com/mattpocock/skills
- 目录:
skills/engineering/tdd/ - 许可证:MIT
- 配套说明:https://www.aihero.dev/skills-tdd
- 分发页:https://skills.sh/mattpocock/skills/tdd
SKILL.md 的 frontmatter 写的触发条件是:用户要先测后写地做功能或修 bug、提到 red-green-refactor、或者要写集成测试。仓库 README 对它的一句话是:按垂直切片做测试驱动开发,一次处理一个行为。
它要解决的问题很具体:Agent 默认会「横向切片」——先写完全部测试,再写全部实现。tdd 要求改成「纵向切片」:一个测试 → 一段刚好够用的实现 → 再写下一条。每一圈都是一发 tracer bullet(示踪弹),用上一次循环学到的东西决定下一条测什么。
核心规则¶
Skill 正文要求:下面每一节在每一圈红绿循环里都要看,不是做完再回头补。
什么算好测试¶
测试必须通过公开接口验证行为,而不是验证内部结构。实现可以整段换掉,测试不该跟着动。好测试读起来像规格,例如 "user can checkout with valid cart",一眼能看出系统具备什么能力。
官方 tests.md 给的好例子是走真实调用路径:
// GOOD: 测可观察行为
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
对照下面这条坏例子:它测的是内部协作方式,重构时行为没变,测试却会红。
// BAD: 测实现细节
test("checkout calls paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
Skill 还强调:期望值必须来自独立的事实来源——规格里的字面量、手工算过的例子、需求本身。不能用实现同一套算法再算一遍当期望值。下面这条就是它点名的套套逻辑(tautological)测试:
// BAD: 期望值按代码自己的方式重算,测了等于没测
test("calculateTotal sums line items", () => {
const items = [{ price: 10 }, { price: 5 }];
const expected = items.reduce((sum, i) => sum + i.price, 0);
expect(calculateTotal(items)).toBe(expected);
});
// GOOD: 期望值是独立的已知字面量
test("calculateTotal sums line items", () => {
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);
});
另外,不要绕过接口去查数据库来证明「写入成功」,而要用同一条公开接口把数据再读回来:
// GOOD: 通过接口验证
test("createUser makes user retrievable", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
好测试的特征可以收成几条:测调用方在意的行为、只用公开 API、内部重构后仍能绿、描述 WHAT 而不是 HOW、一条测试一个逻辑断言。
测在 seam 上¶
Skill 借用 Michael Feathers 的 seam(缝隙)这个词:测试要落在公开边界上,在那里观察行为,不要伸进模块内部。测试只写在事先约定好的 seam 上。写任何测试之前,Agent 必须先列出准备测的 seam,并跟你确认;未确认的 seam 不准写测试。
它会问的问题是:「公开接口是什么,我们应该在哪些 seam 上测?」
测不完所有边角。事先约定 seam,是为了把测试力气花在关键路径和复杂逻辑上。接口本身该多深、seam 该放哪、对外暴露什么,这些词来自同仓库的 codebase-design Skill。官方说明写得很清楚:tdd 在 v1.0 删掉了自己那套深模块笔记,改成引用这份共享词汇表;codebase-design 需要一并安装,但它是查阅用的参考,不是要另开一场设计会话。
探索代码时,如果仓库里有 CONTEXT.md,要用里面的领域用词来写测试名和接口名,并遵守相关 ADR。
三种反模式¶
SKILL.md 点名三类写出来不值得留的测试:
- Implementation-coupled(绑死实现):mock 内部协作者、测私有方法、或者走旁路验证(直接查库而不是走接口)。判断标准是:你只是重构,行为没变,测试却红了。
- Tautological(套套逻辑):断言用代码自己的方式重算期望值,例如
expect(add(a, b)).toBe(a + b),或者手工按实现推出来的 snapshot。这种测试天生不会跟代码吵架。 - Horizontal slicing(横向切片):先写完全部测试,再写全部实现。批量测试验证的是想象中的行为,测的是「东西长什么样」而不是用户能做什么,并且在还没理解实现之前就把测试结构锁死了。
正确做法是垂直切片:一个测试 → 一段实现 → 重复。每一条测试都是示踪弹,根据上一圈学到的东西再写下一圈。对照关系可以写成:
WRONG(横向):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT(纵向):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
循环规则¶
当前 SKILL.md 里的循环只有两步:
- Red before green. 先写会失败的测试,再写刚好让它通过的代码。不要提前给后面的测试做准备,也不要加投机功能。
- One slice at a time. 每一圈:一个 seam、一个测试、一段最小实现。
- 重构不在这个循环里。 重构归同仓库的
code-reviewSkill,在 review 阶段做,不塞进红绿实现循环。
这里有一个需要单独说清楚的地方。仓库 README、skills.sh 摘要,以及 SKILL.md 的 description 字段,仍然写着 red-green-refactor。作者在 aihero.dev 的技能说明里解释过:重构步骤在 2026 年 6 月被拿掉了,因为 Agent 几乎从不认真做这一步,而且实现和审查拆成两次会话更合适;描述字段没同步改掉,对应仓库 issue #589。所以你说「red-green-refactor」仍然会触发这个 Skill,实际跑的是红 → 绿,重构交给 code-review。
什么时候该 mock¶
配套文件 mocking.md 规定:只在系统边界 mock。
可以 mock 的:
- 外部 API(支付、邮件等)
- 数据库(有时可以,更推荐测试库)
- 时间 / 随机数
- 文件系统(有时可以)
不要 mock 的:你自己的类和模块、内部协作者、任何你能控制的东西。
为了让边界可 mock,官方给了两条设计建议。
第一,依赖注入,不要在函数内部 new 出外部客户端:
// 容易 mock
function processPayment(order, paymentClient) {
return paymentClient.charge(order.total);
}
// 很难 mock
function processPayment(order) {
const client = new StripeClient(process.env.STRIPE_KEY);
return client.charge(order.total);
}
第二,给每个外部操作单独的函数,不要做一个带一堆分支的通用 fetch:
// GOOD: 每个函数可以单独 mock
const api = {
getUser: (id) => fetch(`/users/${id}`),
getOrders: (userId) => fetch(`/users/${userId}/orders`),
createOrder: (data) => fetch("/orders", { method: "POST", body: data }),
};
// BAD: mock 时要在内部写条件
const api = {
fetch: (endpoint, options) => fetch(endpoint, options),
};
安装与启用¶
tdd 遵循通用的 SKILL.md 格式,Cursor、Codex CLI、Claude Code 等支持 Agent Skills 的工具都可以用。官方提供两条安装路线,README 写明二选一,两套都装会得到两份重复的 Skill。
仓库目录大致是:
skills/engineering/tdd/
SKILL.md
tests.md
mocking.md
agents/openai.yaml
tests.md 和 mocking.md 是循环中按需查阅的参考,不是可执行脚本。
1. 只装 tdd(以及它依赖的 codebase-design)¶
skills.sh 上这个 Skill 的安装命令是:
npx skills add https://github.com/mattpocock/skills --skill tdd
作者在 aihero.dev 写过:tdd 在 v1.0 之后依赖 codebase-design 提供 seam / 深模块那套词汇,需要一并安装。tdd 本身是无状态的,不会往仓库里写文件。
2. 用 skills.sh 装整套,再按需勾选¶
这是 README 给 Codex 以及其他 Agent 的默认方式,Cursor 也可以用。安装器会让你选择 Skill 和目标 Agent,并把文件写进仓库,之后可以改:
npx skills@latest add mattpocock/skills
勾选时把 tdd 带上。如果还要用同一套工程技能(拆票、实现、审查),README 要求同时勾选 setup-matt-pocock-skills,装完后在 Agent 里跑一次 /setup-matt-pocock-skills,配置 issue 跟踪器、triage 标签和文档存放位置。只单独用 tdd 做红绿循环,不必走完这套仓库配置。
更新已拷贝到本地的文件:
npx skills update
3. Claude Code 插件(整套只读、跟随上游更新)¶
当前 README 写的是:这套 Skill 已进入 Claude Code 官方 marketplace,不必先加源。
claude plugins install mattpocock-skills
会话里也可以:
/plugin install mattpocock-skills
插件装的是整套只读包,会随作者发布更新。不要和 skills.sh 那条路线混装。
Cursor 如何发现它¶
Cursor 会从下面这些目录自动加载 Skill:
| 位置 | 范围 |
|---|---|
.agents/skills/、.cursor/skills/ |
当前项目 |
~/.agents/skills/、~/.cursor/skills/ |
当前用户全局 |
.claude/skills/、.codex/skills/ 以及对应的家目录 |
兼容 Claude Code / Codex |
每个 Skill 是一个包含 SKILL.md 的文件夹。npx skills 会按你勾选的 Agent 把文件写到对应目录。也可以手动把官方目录拷到项目里,例如 .cursor/skills/tdd/SKILL.md。在 Cursor 的 Agent 对话框输入 /,搜 tdd 即可手动调用。
典型用法¶
单独调用¶
有一个已经说得清的行为——有输入、有可观察输出——直接 /tdd。也可以在对话里写「先写失败测试再实现」「按 TDD 做」「要集成测试」,让 Agent 按 description 自己选中它。
官方期望你看到的过程是:
- Agent 先列出准备测试的公开 seam,停下来等你确认。没有确认之前不写测试文件。
- 写一条会失败的测试,确认它是因为行为还不存在而红,不是测试本身写错。
- 只写刚好让这一条通过的实现。
- 再写下一条。不要一次丢进一批测试。
第一条测试就是示踪弹:先证明有一条端到端路径是通的,再往外长。
放进完整工程链¶
同一仓库里,tdd 是构建步骤内部的引擎,不是单独的「做完所有事」步骤。主链路在官方文档里写成:
grill-with-docs → to-spec → to-tickets → implement → code-review
含义是:to-spec 先约定测试 seam;implement 按工单驱动 tdd;code-review 检查是否只在约定过的 seam 上写了测试,并承担 tdd 不再做的重构。手头已经有 spec 或工单、想一次跑完构建时,官方建议跑 /implement,而不是单独拿 /tdd 当整段工作流。
没有完整 spec、只想先测后写某一个具体行为时,直接 /tdd 即可。
怎样算它在工作¶
官方给出的验收信号包括:
- 任何测试文件出现之前,它会停下来报 seam 并等待。
- 一次只出现一条测试,先红再绿,然后才写下一条;不是一批测试配一批代码。
- 测试名读起来像能力(
user can checkout with valid cart),不像内部步骤(checkout calls paymentService.process)。 - 断言里的期望值能追溯到规格或已知例子,不是按实现重算出来的。
- 给内部函数改名,测试套件不该跟着红。
- mock 只出现在外部边界(支付 API、时钟),不包你自己的模块。
适用场景与注意事项¶
适合用 tdd 的情况:行为已经钉住,有明确输入和可观察输出。官方举例包括业务逻辑、请求/响应契约、数据变换、校验。
不适合、或者说官方自己标成缺口的情况:配置、接线、胶水代码、纯类型标注、直接把 CRUD 转交给下层。这类改动往往没有独立的事实来源可以断言,硬跑循环容易写出 Skill 自己警告过的套套逻辑测试。这件事对应仓库 issue #746,文档写明在关闭之前,要不要走 TDD 由你或仓库的 CLAUDE.md 决定。
另外几条官方已经记录的限制,用的时候值得提前知道:
- Agent 仍可能先写实现。 技能说明里写过:有人追问模型为什么没先写测试,得到的回答是「我读了规则,但还是回到了平常的习惯」。Skill 不会 100% 强制执行。某一刀必须严格红先于绿时,需要盯着这次运行,而不是假设文件在就能保证纪律。
- 不要默认从浏览器 / E2E 测起。 有用户遇到 Agent 先写 Playwright 测试,再长时间循环,最后判断是测试坏了——而功能当时还不存在。浏览器测试太慢,红绿反馈会亏本。官方建议在
CLAUDE.md里写明:行为先在更快的测试里做稳,再补浏览器测试。 - 选 seam 会卡住。 这是反馈最多的摩擦点(issue #607)。提示往往只给候选 seam 的名字,不说明各测到什么、漏掉什么。实用做法是先让 Agent 讲清楚:组件级 seam 漏什么、集成级 seam 慢多少,再做选择。完整链路里这件事会提前在
to-spec里定掉。 - 它看不到其他工单。 对着一张票跑时,它可能提出属于兄弟工单的工作(issue #129)。作者的立场是这不是
tdd的职责。把 spec 和工单一并给它,或者把工单本身切到合适的大小,更有效。 /tdd不替代/implement。 课程里原来的/do-work现在拆成/implement、/tdd、/code-review。对着一张票该跑哪一个,官方几乎总是回答/implement。
小结¶
tdd 做的事情很窄:把 Agent 从「先铺完全部测试再填实现」拉回「一条测试、一段实现、再下一条」。测试落在事先约定的公开 seam 上,断言对着可观察行为,mock 停在系统边界。重构不在这个循环里,接口深度那套词也不在这个文件里,分别交给 code-review 和 codebase-design。
它管不住模型偶尔偷跑去先写代码,也决定不了「这个改动值不值得测」。能做的是:当你已经知道要验证哪条行为时,给 Agent 一份每圈都要查阅的规则,让留下来的测试更像规格,而不是实现的影子。
官方地址:
- Skill 目录:https://github.com/mattpocock/skills/tree/main/skills/engineering/tdd
- 技能说明:https://www.aihero.dev/skills-tdd
- 安装页:https://skills.sh/mattpocock/skills/tdd
- 仓库:https://github.com/mattpocock/skills