我的 Skill 让 Claude Code 非常擅长 TDD
这是我的 TDD Skill,它通过红—绿—重构和垂直切片,让 Claude 编写诚实的测试与真实实现。
本页目录
过去几周,我一直使用自己编写的 TDD Skill 完成大部分非前端工作。
它解决了我以前在 LLM 和测试方面遇到的许多问题。
如果想尝试,内容如下:
npx skills add mattpocock/skills/tdd
下面是更详细的拆解:
问题:为什么 LLM 不擅长测试
让 LLM“编写一个功能”时,它往往采用 水平切片:先编写完整功能,再补写测试。问题在于,这并不能真正验证测试是否检查了它们应该检查的内容。
实际过程如下:
| 水平切片(❌ 不好) | 垂直切片(✅ 好) |
|---|---|
| 红:编写全部测试 | 红→绿:test1→impl1 |
| 绿:编写全部代码 | 红→绿:test2→impl2 |
| 重构:清理代码 | 红→绿:test3→impl3 |
核心问题是: 批量编写的测试检查的是想象中的行为,而不是观察到的行为。
LLM 先生成 10 个测试,再实现代码让它们全部通过时,可能出现几个问题:
- 测试验证 Mock,而不是真实代码路径
- 测试可能根本没有正确运行,或内置了短路逻辑
- LLM 上下文不足时,可能直接改写测试让它通过,而不是编写真正的实现
坏测试不只是评审问题,更是债务问题。每个测试都要像代码一样长期维护。没有绑定实际行为、或与实现细节耦合过深的测试,会变成昂贵负担。
解决方案:红—绿—重构的垂直切片
我的 TDD Skill 会约束 Claude 使用 垂直切片 ,并采用 Tracer Bullet:
ONE test → ONE implementation → repeat
每个循环都会回应上一个循环中学到的内容。因为代码刚刚写完,所以能准确知道哪些行为重要、该如何验证。
三个阶段
红:编写一个失败测试,只写一个。
绿:只编写让该测试通过的最少代码,不做任何猜测性实现。
重构:所有测试通过后,清理重复并简化代码。
这种约束可以防止作弊。测试先失败后,LLM 无法伪造结果,只能编写真正实现。
Skill 如何改变 Claude 构建的内容
在真实功能上使用这种方式时,会发生一件有趣的事:测试会变成 Claude 与自己代码之间的对话.
每个测试都会针对实现提出不同问题:
- “这个可观察行为有效吗?”
- “系统如何处理边缘情况?”
- “条件变化时会发生什么?”
这种追问让 Claude 在推进过程中发现自身实现的特点,而不是机械勾选清单。有时新测试会立即通过,不是因为测试多余,而是实现已经足够健壮。
怎样才算好测试(与坏测试的区别)
Skill 中详细说明了好测试和坏测试,内容如下:
好测试
好测试通过 公共接口运行真实代码路径,而不是检查实现细节。它们描述系统做什么,而不是如何做。
// GOOD: Tests observable behavior through the interfacetest("user can checkout with valid cart", async () => {const cart = createCart();cart.add(product);const result = await checkout(cart, paymentMethod);expect(result.status).toBe("confirmed");});
好测试读起来像规格说明:“用户可以用有效购物车结账”会明确告诉你系统具备什么能力。这些测试不关心内部结构,因此能经受完整内部重构。
坏测试
坏测试会 与实现耦合。它们 Mock 内部协作者、测试私有方法,或不通过接口而采用外部方式验证。
// BAD: Tests implementation detail (mocking internals)test("checkout calls paymentService.process", async () => {const mockPayment = jest.mock(paymentService);await checkout(cart, payment);expect(mockPayment.process).toHaveBeenCalledWith(cart.total);});// BAD: Bypasses interface to verify (queries DB directly)test("createUser saves to database", async () => {await createUser({ name: "Alice" });const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);expect(row).toBeDefined();});
警告信号是:行为没有变化,测试却在重构后失败。如果只是重命名内部函数就导致测试失败,那么测试检查的是实现,而不是行为。
关键区别
| 好测试 | 坏测试 |
|---|---|
| 通过公共接口运行真实代码 | Mock 内部协作者 |
| 描述系统做什么 | 测试系统如何实现 |
| 内部重构后仍保持不变 | 行为未变却在重构时失败 |
| 读起来像规格说明 | 测试数据结构的形状 |
| 关注面向用户的行为 | 通过外部手段验证(DB 查询、调用次数) |
规划阶段(编写任何代码之前)
实践中我发现,在编写任何代码前加入规划阶段极其重要,因此使用以下问题来实现:
- 需要哪些接口更改? 要添加或修改哪些函数、方法或 API?
- 哪些行为最重要? 无法测试一切。优先检查关键路径和复杂逻辑,而不是边缘情况。
- 能否围绕深模块进行设计? 深模块拥有小接口,却在内部处理复杂逻辑,因此测试更简单,API 也更清晰。
- 能否为可测试性进行设计? 函数应该接收依赖而不是自行创建依赖,应该返回结果而不是产生副作用。
用户对这些问题回答得越好,代码质量就越高。
TDD 只是其中之一,还有更多 Skills。
这是我日复一日配合 Claude Code 使用的完整技能套件。
为什么这对 Claude Code 用户很重要
这个 Skill 追求的不是完美测试,而是 通过强制约束获得诚实测试.
把 Claude 的工作组织为“一个测试、一个实现、不断重复”,可以防止它:
- 编写想象中的行为,而非观察到的行为
- Mock 内部实现并伪造测试通过
- 一开始就过度设计方案
- 编写与实现细节耦合的测试
测试因此变得可信。你可以把大量工作委派给 Claude,不只进行代码评审,还包括真正的功能构建,因为你知道测试是诚实的。
能够信任测试时,也就能够信任代码。