拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于测试接缝的 TDD 实践指南:GitHub_Trending/skills13/skills 中 `tdd` 技能的 Red-Green 循环与反模式解析

基于测试接缝的 TDD 实践指南GitHub_Trending/skills13/skills 中tdd技能的 Red-Green 循环与反模式解析【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skillstdd是 Matt Pocock skills 仓库中一套以测试接缝seam为中心的测试驱动开发参考规范它定义了一条先写失败测试、再写恰好够用的实现、逐条推进的 Red-Green 循环并给出好测试的标准、Mock 的使用边界以及三类会悄悄毁掉测试套件的反模式。读完本文你将掌握如何在既有技能链to-spec → implement → code-review中落地这套循环理解只在预先约定的接缝上写测试这一绝对规则并能够用仓库提供的 tests.md 与 mocking.md 中的 TypeScript 示例直接指导日常开发。一、tdd 是什么一套参考而不是一个驱动在 docs/engineering/tdd.md 的定位里tdd通过测试优先的方式构建功能或修复缺陷一次一个失败测试然后写恰好够让测试通过的代码再进入下一个行为。它承载的是让这个循环产出值得保留的测试的标准什么是一个好测试、测试放在哪里、Mock 用来做什么以及三类会悄悄毁掉测试套件的反模式。两个关键约束定义了它的性格不在未经同意的接缝上写任何测试。在任何一个测试存在之前它会先点名打算测试的公共边界并停下来等你确认。原因是测试投入是有限的应该把精力花在关键路径上而不是每一个边缘用例上。tdd是参考reference不是驱动driver。它只持有循环的规则真正运行会话的是你本人或者由 implement 技能按 ticket 逐个驱动它。这一点在 tdd/SKILL.md 的元信息里得到了印证该技能被归类为Model-invoked模型可自动调用其 frontmatter 描述为--- name: tdd description: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions red-green-refactor, or wants integration tests. ---也就是说模型会在任务符合测试优先构建功能/修复 bug提到 red-green-refactor或需要集成测试时自动抓起这个技能——这是它区别于 implement、to-spec 等用户显式调用的技能disable-model-invocation: true的关键差异见 skills/engineering/README.md 的分类说明。二、何时使用 tdd先判断行为是否已被钉住触发方式是输入/tdd或由 Agent 在任务契合时自动选用。它的适用判据非常具体存在一个具有输入和可观察输出的具体行为并且你希望测试能在重构中存活。原文档给出了一张场景 → 去向的决策表完整继承如下你的场景该去哪里有明确输入/输出的行为业务逻辑、请求/响应契约、转换、校验tdd行为还没被钉住to-spec它同样会在写任何代码前先约定测试接缝真正的问题是接口的形态而不是测试codebase-design已有 spec 或 tickets想让人把整个构建跑完implement它按 ticket 驱动tdd配置、粘合代码、类型注解、直白的 CRUD 委托这里没有合适的技能见下方空白说明最后一行是真实的空白不是风格偏好。这个技能决定的是接缝放在哪里没有任何东西决定这次变更是否值得跑这条循环。对一个没有独立真值来源可断言的变更跑它得到的将是一个复述实现的测试——即技能自己警告的同义反复tautological反模式只不过是从另一个方向撞上的。原文档记录这对应 upstream 仓库的 issue #746当前仍为打开状态。在该 issue 关闭前这个判断要由你或你的CLAUDE.md承担。三、前置条件与 codebase-design 共享接口词汇原文档明确列出一个前置条件需要安装 codebase-design。历史背景tdd曾自带 deep-module深模块与接口设计笔记在 v1.0 中这些内容被删除统一收进codebase-designtdd现在借用它来获得接口设计的词汇。除此之外不需要任何别的东西——该技能是**无状态stateless**的不写任何自己的文件。tdd的 SKILL.md 也明确指示当接口的形态本身存疑模块该多深、接缝放哪里、接口该暴露什么时应调用 Skill 工具加载codebase-design的词汇表。这份词汇表见 codebase-design/SKILL.md严格定义了本技能使用的术语模块Module任何有接口 有实现的东西刻意与规模无关函数、类、包、跨层的切片。接缝SeamMichael Feathers 术语可以在不修改该处的前提下改变行为的位置即模块接口所在的地点。深度Depth接口层面上的杠杆——调用者每学一点接口就能支配的行为量。适配器Adapter在接缝处满足接口的具体东西描述的是角色而非实质。这也解释了为什么tdd反复强调测试穿过与调用者相同的接缝——codebase-design 中接口即测试面The interface is the test surface原则正是tdd接缝理论的底层支撑。四、循环与它运行的接缝三个关键词原文档用三个词承载这个技能Red-green红-绿。先写失败测试然后只写恰好够让测试通过的代码不提前预判后一个测试。需要特别注意的是这个循环没有重构阶段。原文档记载重构阶段在 2026 年 6 月被移除原因是 Agent 实际上几乎从不执行它而且把评审与实现拆成独立会话效果更好。重构现在归属于 code-review。垂直切片Vertical slice。一次一个接缝、一个测试、一个最小实现然后重复第一个循环是一条曳光弹tracer bullet用来证明单一路径端到端跑通。与之相反的是水平切片horizontal slicing先写完所有测试再写所有代码。批量测试验证的是想象出来的行为——它们检查的是事物的形状而不是用户真正做的事而且会在你理解实现之前就把测试结构锁定下来。预先约定的接缝Pre-agreed seam。接缝是你观察行为所经由的公共边界无须窥探内部。规则是绝对的不在未确认的接缝上写测试。在完整链路中接缝会在更早的 to-spec 阶段就约定好——原文档引用链路原话/tdd被指示只在预先约定的测试接缝上工作/code-review检查是否只使用了约定的接缝。单独调用tdd时它会直接向你询问。4.1 三条循环规则源自 SKILL.mdtdd/SKILL.md 把循环规则浓缩为三条每个循环都必须遵守先红后绿Red before green先写失败测试再写恰好够用的实现。不要预判未来的测试不要加投机性的功能。一次一个切片One slice at a time每个循环只做一个接缝、一个测试、一个最小实现。重构不属于循环重构归评审阶段见code-review技能不属于 red → green 实现循环。这与原文档中red-green-refactor 的触发短语与正文不一致的问题直接相关详见第六节的常见问题。五、三类反模式实现耦合、同义反复、水平切片原文档给出了一张反模式速查表完整继承反模式特征实现耦合Implementation-coupled重命名内部函数时测试就挂了尽管行为没变。Mock 内部协作者、断言调用次数、用数据库查询来验证而不是用接口。同义反复Tautological期望值是用代码算它的方式算出来的测试因构造而通过。期望值必须来自别处已知正确的字面量、手算的例子、spec。水平切片Horizontal slicing一批测试在实现之前就落地了。下面结合 tdd/tests.md 中的真实示例逐一展开。5.1 实现耦合测试跟着内部结构一起碎掉// BAD: 测试实现细节 test(checkout calls paymentService.process, async () { const mockPayment jest.mock(paymentService); await checkout(cart, payment); expect(mockPayment.process).toHaveBeenCalledWith(cart.total); });红线信号包括Mock 内部协作者、测试私有方法、断言调用次数/顺序、重构后行为未变但测试崩掉、测试名描述 HOW 而不是 WHAT、绕开接口用外部手段验证。// BAD: 绕过接口去验证 test(createUser saves to database, async () { await createUser({ name: Alice }); const row await db.query(SELECT * FROM users WHERE name ?, [Alice]); expect(row).toBeDefined(); });同样行为的接口式写法// GOOD: 通过接口验证 test(createUser makes user retrievable, async () { const user await createUser({ name: Alice }); const retrieved await getUser(user.id); expect(retrieved.name).toBe(Alice); });5.2 同义反复期望值由构造而通过// 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); });同义反复的典型形态还有手算方式和代码一样的快照snapshot常量断言它等于自己。SKILL.md 的表述是这类测试因构造而通过永远不可能与代码产生分歧——因此也就没有任何验证价值。5.3 水平切片批量测试验证的是想象中的行为先写所有测试、再写所有实现会导致三宗罪测试验证的是想象中的行为测试的是事物的形状而非用户可见的行为测试对真实变更变得不敏感以及在理解实现之前就承诺了测试结构。对策是垂直切片一个测试 → 一个实现 → 重复每个测试都是一颗曳光弹回应上一个循环教给你的东西。六、Mock 的边界只在系统边界从不包住自己的模块原文档的规则一句话Mock 只用于系统边界——外部 API、时间、随机性、有时还有文件系统或数据库。不要 Mock 你自己的模块。tdd/mocking.md 给出了更细的清单可 Mock 的系统边界外部 API支付、邮件等数据库有时——更倾向测试库时间/随机性文件系统有时不可 Mock 的你自己的类/模块内部协作者任何你控制的东西6.1 为可 Mock 性设计接口在系统边界处要设计容易 Mock 的接口两个具体手法手法一依赖注入。把外部依赖作为参数传进来而不是在内部创建// 易 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); }手法二优先 SDK 风格的接口而不是通用 fetcher。为每个外部操作创建具体函数而不是用一个带条件逻辑的通用函数// 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), };SDK 方式的收益每个 mock 只返回一种具体形状、测试 setup 里没有条件逻辑、更容易看出测试覆盖了哪些端点、每个端点都有类型安全。七、什么是一个好测试读起来像一份规格说明书tdd/SKILL.md 开篇定义测试通过公共接口验证行为而不是验证实现细节。代码可以彻底改变测试不应改变。一个好的测试读起来像规格说明书user can checkout with valid cart精确告诉你存在什么能力并且因为不关心内部结构它能挺过重构。// 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); });好测试的特征源自 tests.md测试用户/调用者关心的行为只用公共 API挺过内部重构描述 WHAT而不是 HOW每个测试一个逻辑断言tdd在探索代码库时还有一个硬性习惯如果存在CONTEXT.md就先读它让测试命名和接口词汇与项目的领域语言对齐并尊重所触及区域的 ADR。八、常见问题原文档的七个 QA原文档记录了大量真实使用中的摩擦与决策完整继承如下。Q1为什么不重构描述明明写着red-green-refactor。因为重构步骤被移除了而描述没有被更新。移除是刻意的Agent 实际上几乎从不执行重构把实现与评审放在独立会话中效果更好。与其纠结结果还算不算教科书意义上的 TDD不如关心循环是否产出了更好的代码。red-green-refactor这个触发短语与正文的不一致被记为 upstream issue #589仍为打开状态因此这个短语继续作为触发tdd的有效词。你实际得到的是 red → green重构交给 code-review。Q2它让我选测试接缝我却完全不知道选哪个。这是对该技能反馈最多的一处摩擦upstream issue #607。提示只按名称列出候选接缝对每个接缝能捕捉什么、会漏掉什么只字未提你等于在几个标签之间做选择。目前还没有修复方案。实用的变通办法是在回答之前先让 Agent 说明取舍——组件级接缝会漏掉哪些集成级接缝能捕捉的东西以及集成级会慢多少。这也是链路要在to-spec阶段就预先约定接缝的原因——在to-spec里你能看到整个功能的全局而不是面对一条孤立的提示。Q3技能说先红它却先写了实现。确实会发生。原文档记载了一位用户追问模型后得到的异常诚实的回答我知道技能说了一次一个测试看着它因正确的原因失败。我读了。我只是默认回到了我的日常习惯。这个技能就是为此而写的没有任何指令能让 Agent 100% 遵守把要求逼得更紧只会以很小的收益限制 Agent 的创造力即使没有被严格遵循这条循环仍然值得跑因为整体结果仍然更好。如果某个特定切片必须严格遵循那就盯着运行过程而不是指望技能强制执行。Q4应该先写浏览器或端到端测试吗通常不应该而且技能不会阻止你。原文档记录了一个真实案例用户让 Agent 先写 Playwright 测试结果在功能根本还不存在时花了很长循环反复重跑、并错误地断定是测试坏了。请在仓库的CLAUDE.md里配置这一点。浏览器测试慢到让 red-green 反馈循环不再划算应在行为可用之后再写它们。Q5/tdd会取代/implement或课程里的/do-work吗不会。/tdd记录方法论/implement是一个极简的工作 → 反馈 → 提交循环是/do-work的直接替代品。课程中单一的/do-work步骤现在被拆到了/implement、/tdd和/code-review三个技能里。如果问对一个 ticket 该跑哪一个答案几乎总是/implement。Q6deep-modules 和接口设计指导去哪里了在 v1.0 中收进了 codebase-design被泛化成多个技能共享的一套词汇。refactoring.md也在同一时间离开重构现在是 code-review 的职责那个技能带走了 Fowler 的坏味道基线。Q7它知道我的其他 ticket 吗不知道。对着一个 ticket 运行它会乐意提出属于兄弟 ticket 的工作因为它看不到 issue 图的其余部分upstream issue #129。Matt 的立场是这不是tdd的职责。把 spec 与 ticket 一起传给它会有帮助先把 ticket 切分得大小合适帮助更大。九、怎样算起作用了可验证的成功清单原文档给出了明确的验收标准完整继承在任何一个测试文件存在之前它停下来、点名它打算测试的接缝然后等待。一次出现一个测试变红、拿到恰好够用的代码使其通过然后才出现下一个测试——而不是一批测试 一批代码。测试名读起来像能力user can checkout with valid cart而不是像内部结构checkout calls paymentService.process。断言里的期望值是能追溯到 spec 的字面量而不是用代码的方式重算出来的值。重命名一个内部函数套件里什么都不碎。Mock 只出现在外部边界支付 API、时钟从不围绕你自己的模块。十、它在哪里构建步骤中的引擎而非独立环节tdd是主链构建步骤内部的引擎而不是独立的一步grill-with-docs → to-spec → to-tickets → implement → code-review各环节的分工均有仓库源码佐证to-spec在写任何代码之前预先约定测试接缝。其流程第 2 步明确要求草拟你将在其上测试该功能的接缝。优先采用既有接缝使用尽可能高的接缝理想数量是一个并与用户确认这些接缝符合预期。implement按 ticket 驱动tdd。其正文第一句就是Use /tdd where possible, at pre-agreed seams.并规定定期跑类型检查、定期跑单个测试文件、最后跑一次完整测试套件完工后用/code-review评审再提交。code-review事后检查是否只用了约定的接缝并接管tdd不再做的重构。它的双轴评审Standards / Spec保证了只用了约定接缝这件事被独立核查。另一个邻居 codebase-design是tdd所讲的接缝与深模块词汇的共同来源。此外tdd也可以单独使用只要有一个具体行为要构建、且没有完整的 spec 在案。当你拿不准该用哪个技能时ask-matt 负责路由。十一、落地建议把这条循环带进你自己的仓库综合原文档与仓库源码以下是可直接执行的落地清单判断是否值得跑循环先确认变更存在独立于实现的真值来源spec、已知正确的字面量、手算例子。没有它宁可先走 to-spec。先约接缝再写测试无论单独用/tdd还是在implement驱动下都坚持点名接缝 → 用户确认 → 才写测试的次序接缝尽量少理想情况只有一个。垂直切片推进一次一个接缝 测试 最小实现让第一个测试当曳光弹。Mock 只放在系统边界外部 API、时间、随机性、必要时文件系统/数据库用依赖注入和 SDK 风格接口让边界易于替换。把浏览器测试后置写进CLAUDE.md避免慢反馈循环吃掉 red-green 的价值。重构交给 code-review实现阶段只管 red → green重构作为独立会话由 code-review 承接。这套循环的价值不在于字面意义上的严格 TDD而在于它持续产出能挺过重构、有独立真值来源、只在系统边界使用 Mock 的测试——这正是原文档在反模式与成功清单两节中反复强调的底线。【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门