Matt Pocock 的 Skills:122k Star 和一个真正管用的 TDD 方法

Matt Pocock 的 Skills 仓库有 222k 安装量和 122k star,不是因为什么革命性的创新。它受欢迎的原因很朴素:它修好了每个 AI 编码 Agent 用户都会遇到的一个具体问题——生成的代码跑不通,而且你不跑起来永远不知道。

这个仓库收录的是 Matt 日常 .claude 工作流中沉淀下来的 agent skill。做大应用很难。外部 API 会挂,数据库会死锁,竞态条件只在高并发时出现。GSD、Spec-Kit 这类方法论尝试通过重量级的阶段闸门来解决,但代价是让你失去控制权——你不再是工程师,变成了项目调度员。Matt 的做法反其道而行:小而可组合的 skill,按需取用。不想用测试 skill?不装就行了。没有任何流程仪式感。

其中 TDD skill 是最精心打磨的一个,支撑文档最多(包含专门的 tests.md 好/坏测试示例和 mocking.md mock 指南),对代码质量的提升也最直观。装一次,在 agent 里跑 /tdd,下次让它写功能,它会一次写一个失败的测试,实现够通过的代码,然后进入下一个行为。不再是有着满屏绿色 CI badge 但什么都跑不通的应用。

为什么 AI Agent 写出的测试是垃圾

当你让 AI Agent实现一个功能时,它通常产出这样的东西:

test verifiesBilling()
test verifiesShipping()
test verifiesTaxCalculation()
test appliesDiscount()
test handlesRefund()
→ [然后实现全部 handler]

先写全部测试,再写全部代码。Matt 称之为「横向切分」,诊断是准确的。横向切分把 RED 当作「写完所有测试」,GREEN 当作「写完所有代码」。这产生了双输局面:测试脆弱(因为从未和生产代码配对),代码没有测试驱动的形状(因为没有在测试约束下成长)。

问题会层层展开:

批量写的测试倾向于 mock 内部依赖,因为 mock 比通过参数把真实依赖穿进系统容易得多。它们测试私有方法,因为 mock 依赖需要先重构生产代码,而 AI Agent 不想在「写测试之前」做额外工作。它们通过 mock spy 验证调用次数和参数顺序,因为这是测试框架惯例式鼓励的写法:.toHaveBeenCalledTimes(1).toHaveBeenCalledWith(expected)

但一个关心调用次数的测试测试的是实现细节。它说「这个函数调了那个函数一次」,不说「这是用户和调用方关心的可观察行为」。红线很明显:如果你的测试在重构(没改行为)时破裂,说明它测试的是实现,不是行为。 你重命名一个内部函数,测试挂了——这不代表有问题,这代表你的测试在测实现细节。

纵向切片:纠正方向

TDD skill 的做法是反过来。每个功能都纵向切分:

# 横向(坏)
RED:  test1()  test2()  test3()  test4()  ← 先写完所有测试
GREEN: codeA()  codeB()  codeC()          ← 再写完所有代码
REFACTOR: 啥也不敢改因为测试不可靠
# 结果:很多测试,一个能用的功能都没有

# 纵向(好)
RED:   test1()
GREEN: → codeA()
REFACTOR: → 清理
RED:   test2()
GREEN: → codeB()
REFACTOR: → 清理
# 结果:每次 red-green-refactor 循环产生一个可用增量

纵向切片强制增量反馈。每次通过的测试验证一个真正可用的功能。你永远不会到达「测试全过但什么都不工作」的状态。当 AI Agent 写了 test("用户可以用有效购物车结账") 然后立刻实现 checkout() 让它通过,你有了一个能用的结账功能。然后下一个测试在已有的函数上扩展:test("折扣在结账时应用")。API 自行生长,由实际被测试的行为驱动。

三条规则

这个 skill 的哲学建立在三条相互关联的规则上。

规则一:通过公开接口验证行为,不验证实现细节。 代码可以完全重写,测试不应该变。好的测试读起来像用领域语言写的说明书:「用户可以用有效购物车结账」告诉了你确切的存在什么能力,不提及任何内部函数名或接口。坏的测试读起来像实现 walkthrough:「checkout 调用了 paymentService.process」或「createUser 给用户表保存了一行」。第一个告诉你系统 做了什么。第二个告诉你 怎么做的——而 怎么做的 是最可能变化的部分。

规则二:集成测试优于带 mock 的单元测试。 通过真实代码路径、使用公开 API 来测试。测试结账时,创建一个真实的 cart 对象,添加一个真实的产品,调用 checkout(),断言返回值。不要 mock paymentService 然后断言 paymentService.process() 收到某些参数。集成测试告诉你系统能工作。mock 测试告诉你两个内部函数互相说话了。

只在系统边界做 mock:外部支付 API(Stripe、PayPal)、邮件服务、存储供应商、需要时查数据库、文件系统、时间、随机数。永远不要 mock 你自己的类、你自己的模块或内部依赖。如果一个模块调用你代码库里的另一个模块,通过调用方的 public API 测试它,让被调用方自己跑自己的测试。当你到达无法控制的边界(网络调用、数据库查询、文件写入),为那个边界设计接口并注入或 mock。

检验标准极其简单:重命名一个内部函数或把私有方法移到不同文件。如果测试挂了,它测的就是实现。

规则三:先规划再编码。 skill 包含一个规划阶段,让你在写任何代码之前明确确认接口变更、优先排序要测试的行为(从最风险或不确定的开始)、设计可测试性。这不是文档仪式。实践中它长这样:「我的计划是生成 PDF 发票。新 public API 应该是 generateInvoice(order) -> PDF。要测试的行为是:单物品正常路径、多物品折扣、空购物车返回错误。我要触碰的模块是 invoiceService.ts,它调用 stripeService(系统边界)。」这三十秒。它捕获三种错位:接口形状不对、行为优先级不对、目标模块不对。最坏情况花三十秒规划了一个错误的方向。最好情况避免了花一个小时生成又决定删除的无用代码。

好测试 vs 坏测试——具体对比

skill 的 tests.md 给出了有清晰 before/after 对比的具体例子。完整模式如下:

好测试——集成风格,仅公开 API,一个逻辑断言:

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");
});

测试可观察行为。仅使用 public API。内部重构后仍然通过。描述 什么,不描述 怎么。每个测试一个逻辑断言。

坏测试——mock 内部依赖:

test("checkout calls paymentService.process", async () => {
  const mockPayment = jest.mock(paymentService);
  await checkout(cart, payment);
  expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});

红旗信号:mock 了内部依赖,测试了内部函数调用,测试名描述的是 怎么 不是 什么,验证了调用次数。

坏测试——绕过接口直接验证外部状态:

test("createUser saves to database", async () => {
  await createUser({ name: "Alice" });
  const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
  expect(row).toBeDefined();
});

// 好的替代:通过接口验证而非直接查数据库
test("createUser makes user retrievable", async () => {
  const user = await createUser({ name: "Alice" });
  const retrieved = await getUser(user.id);
  expect(retrieved.name).toBe("Alice");
});

这个模式在 AI Agent 生成的代码中到处可见。 AI Agent 尝试通过直接查询数据库来验证写入。正确做法是通过 public getUser() API 查询。相同的信息。不同的意图:一个测试实现,一个测试行为。

为可 mock 性设计:两种模式

在系统边界,skill 推荐两种让测试更容易但不增加复杂性的模式:

依赖注入。 把外部依赖作为参数传入,而不是在函数内部创建:

// 方便在测试中 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);
}

依赖注入版本只需要多一个参数。测试中传入 mock。生产中传入 new StripeClient(process.env.STRIPE_KEY)。这不是样板代码——这是可隔离性。

SDK 风格接口优于通用 fetcher。 为每个外部操作创建特定类型的函数,而不是一般的 fetch(uri, options) 然后需要条件逻辑来 mock:

// 好:每个函数独立可 mock,形状单一
const api = {
  getUser: (id: string) => fetch(`/users/${id}`),
  getOrders: (userId: string) => fetch(`/users/${userId}/orders`),
  createOrder: (data: CreateOrderInput) => fetch('/orders', { method: 'POST', body: data }),
};

// 坏:mock 需要在 mock 实现中写条件判断
const api = {
  fetch: (endpoint: string, options: RequestInit) => fetch(endpoint, options),
};

SDK 风格有三个好处:每个 mock 返回一种固定形状(测试 setup 不需要条件判断),你能一眼看出测试访问了哪些 endpoint,每个 endpoint 有独立的类型签名保证类型安全。

Red-Green-Refactor 纪律

经典三步 TDD 循环有一个 skill 特别强调的关键规则:只在所有测试通过后重构。 GREEN 阶段让测试变绿。REFACTOR 阶段清理重复代码、提取共享逻辑、加深模块、应用 SOLID 原则——但所有操作都是在测试套件活跃监管的前提下进行的。这不是学究式的 TDD 纯洁性。这是为了避免你重构完代码发现有什么东西静默坏了——因为你删了一个 guard clause 或改了一个返回类型。

skill 围绕三个实际问题组织重构指南:

提取重复代码。 当两个测试以不同参数执行几乎相同的代码路径时,把共享逻辑提取成 helper 函数。不要等到第三次出现才动手。

模块加深。 如果一个函数处理三个不同的职责(验证输入、格式化输出、持久化到存储),拆成多个函数。根深模块暴露简单的接口但内部实现丰富——一个做一件事的 public 函数,内部复杂度藏在干净的边界后面。

SOLID 原则作为事后清理。 skill 把 SOLID 当作 dev-excused 重构手段,不是前置架构设计。你不为单一的职责在写测试前做设计。你写完测试、通过它、看到函数增长职责,然后提取。

实战场景:何时适合,何时不适合

这个 skill 对行为明确且可观察的 feature 和 bugfix 非常优秀。以下是纵向切片 TDD 方法产生最大价值的具体场景:

  • 领域逻辑——定价规则、折扣计算、权限检查、数据转换管道。这些正是最容易错配定义、部署后修复代价高昂的东西。
  • API 端点和请求 handler——每个端点变成一个可测试单元,具有清晰的输入/输出契约。
  • 数据验证流——规则引擎、schema 验证器、表单处理链。每个验证规则独立测试。
  • Bugfix——与其打补丁,不如写一个复现 bug 的测试(红),修复代码让它通过(绿),然后自信地重构(黄)。
  • 重构已有代码——如果一个模块很乱但有测试,skill 的重构指南让你安全地改善它的结构。

以下场景不太适合:

  • 探索性原型开发——当你在 sketch 想法、还不清楚接口时,在写代码前先写测试拖慢的速度多过帮助
  • UI 样式和 CSS——视觉上就能验证的改动不需要自动化测试
  • 纯配置变更——如果改动只是编辑 JSON config 或没有运行时行为的 schema,测试只会增加负担
  • 测试框架还没搭好——skill 假设有可用的测试运行器。如果从零搭建,单独配测试框架的开销对小项目来说可能不值得

安装和运行

通过 npx skills 一行安装:

npx skills add https://github.com/mattpocock/skills --skill tdd

安装全部 skill:

npx skills@latest add mattpocock/skills

安装后在 agent 里运行 /setup-matt-pocock-skills 配置 issue tracker 集成和 triage 标签。然后使用 /tdd 激活一个 session 的 skill。

skill 会自动带上支撑文档:tests.md(好/坏测试示例)和 mocking.md(何时 mock、何时不 mock)。不需要手动引用这些文件——skill 自动加载。

为什么 TDD 对 AI Agent比对人类更重要

人类驱动测试驱动开发已经存在几十年了。Kent Beck 用 Extreme Programming 普及了它。Martin Fowler 写了权威的 TDD 定义文章。成千上万的工程师在数百万个项目上写过 red-green-refactor 循环。方法本身不新。

新的是 AI 编码 Agent天然对 TDD 中真正产生价值的部分有强烈抵触。AI Agent想要展示「完整性」。让实现功能时,它们先产出所有测试再产出所有代码——正好是 skill 警告的横向切分模式。它们倾向于 mock 内部依赖,因为 mock 容易:一行 jest.mock(something) 比把真实依赖通过参数穿进系统简单。它们产出测试去验证调用次数和参数顺序,因为这是阻力最小路径,也是测试框架示例惯例展示的内容。

skill 的约束恰好推翻这些默认行为。纵向切片迫使 AI Agent 在一次测试后停下,实现它,验证通过,然后继续。不再是一次性生成五十个测试然后希望其中一些是对的。只在集成层面测试防止 mock 漫灌让每个测试变成 mock 意面怪胎。规划阶段在 AI Agent 消耗 token 生成你不想要的代码之前就暴露错位。

这些不是细微改进。这是针对 LLM 天然「阻力最小路径」倾向的结构对抗。AI Agent想通过「全面」来显得有帮助。TDD 想通过「增量最小」来产生价值。skill 让AI Agent做它天然不愿意做的事:写更少的代码、更少的测试、更少的一切——直到不得不写更多。

总结

AI Agent 语境下的 TDD,不是关于测试覆盖率百分比或 CI 满绿。它是关于一条让 AI Agent 始终对齐「代码应该真正做什么」的反馈回路。纵向切片保证每个测试对应可用功能。集成测试保证测试在重构后仍然通过。规划保证你在写任何东西之前在做正确的东西。

Matt Pocock 的 TDD skill 不是银弹。你仍然需要验证生成的代码是否合理。你仍然需要读 AI Agent 产出的东西。但 skill 移除了最 dominating 的可信错误来源:一个通过但不在测试你以为是的东西的测试套件。

如果你每天都在用 AI 编码 Agent,装上 TDD skill,在下一个 feature 上跑它。横向切分和纵向切分的区别不是理论上的——当你的测试套件在重命名内部东西后不挂的那一刻,你就能感觉到。那种感觉就是你可以信任的代码。

GitHub: https://github.com/mattpocock/skills | Skills: https://www.skills.sh/mattpocock/skills/tdd

相关文章

22 个 Claude Code 技能打通内容创作全链路:从生成到发布的一条龙工作流

22 个 Claude Code 技能打通内容创作全链路:从生成到发布的一条龙工作流

你写完一篇技术博客,接下来要做的事情让人头疼:生成封面图、画配图、做信息图、转 HTML 适配微信公众号、发到 X 和微博。这些工作以前需要切换四五个工具,现在在 Claude Code 里打一条命令 ...

Caveman:砍掉 AI 输出 75% 的 Token,一个都不少

Caveman:砍掉 AI 输出 75% 的 Token,一个都不少

你的 AI Agent 话太多了。每句"当然!我很乐意帮你解决这个问题"都是一个浪费的 token —— 它在烧钱、拖慢响应速度、把真正的答案埋在客套话里。如果你每天都在用 AI 编码 agent — ...

风格 × 布局:baoyu-skills 的视觉设计系统如何让 AI 画得比你设计得还好

你让 AI 画张图,它给你一堆过饱和塑料感的玩意儿。你让 AI 做信息图,它把文字堆在一张不透明的背景上。你让 AI 设计幻灯片,它给你五颜六色、毫无一致的灾难。 问题的根源不在 AI 不会画画—— ...

跟 Claude 说一声,图就画好了:/drawio 在 Claude Code 里直接出图

跟 Claude 说一声,图就画好了:/drawio 在 Claude Code 里直接出图

你在跟 Claude Code 描述系统架构。它回复了一堆 ASCII art,差不多能看,但总觉得差点意思。你心想:"要是能直接让它画张图就好了。" 可以。 draw.io 的 Claude C ...

当 AI 输出从 10 行暴涨到 1000 行,Claude 团队为什么正在抛弃 Markdown

当 AI 输出从 10 行暴涨到 1000 行,Claude 团队为什么正在抛弃 Markdown

你的 AI 能一次性输出 1000 行计划、画复杂流程图、做完整代码审查。但你还在用 Markdown 读它。 Claude Code 团队工程师 Thariq 最近在 X 上发了一条很直接的推文: ...

edulab: 把数学题变成交互式 3D 课堂

一个学生盯着立体几何题发呆。"求直线 PQ 与平面 ABC 的夹角。" 课本上只有一张静态图。线叠在一起,角看不清楚。你没法旋转它,没法放大看交点,甚至无法直觉判断那个 120° 的答案在空间里到底对 ...