AIHero

    我的 Skill 让 Claude Code 非常擅长 TDD

    这是我的 TDD Skill,它通过红—绿—重构和垂直切片,让 Claude 编写诚实的测试与真实实现。

    Matt Pocock
    Matt Pocock
    本页目录

    过去几周,我一直使用自己编写的 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 interface
    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");
    });

    好测试读起来像规格说明:“用户可以用有效购物车结账”会明确告诉你系统具备什么能力。这些测试不关心内部结构,因此能经受完整内部重构。

    坏测试

    坏测试会 与实现耦合。它们 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 也更清晰。
    • 能否为可测试性进行设计? 函数应该接收依赖而不是自行创建依赖,应该返回结果而不是产生副作用。

    用户对这些问题回答得越好,代码质量就越高。

    AI Hero · 技能系统

    TDD 只是其中之一,还有更多 Skills。

    这是我日复一日配合 Claude Code 使用的完整技能套件。

    查看技能集

    为什么这对 Claude Code 用户很重要

    这个 Skill 追求的不是完美测试,而是 通过强制约束获得诚实测试.

    把 Claude 的工作组织为“一个测试、一个实现、不断重复”,可以防止它:

    • 编写想象中的行为,而非观察到的行为
    • Mock 内部实现并伪造测试通过
    • 一开始就过度设计方案
    • 编写与实现细节耦合的测试

    测试因此变得可信。你可以把大量工作委派给 Claude,不只进行代码评审,还包括真正的功能构建,因为你知道测试是诚实的。

    能够信任测试时,也就能够信任代码。