尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入 TDD Skill:如何在真实项目中执行 red → green 循环并写出值得保留的测试

深入 TDD Skill:如何在真实项目中执行 red → green 循环并写出值得保留的测试 深入 TDD Skill如何在真实项目中执行 red → green 循环并写出值得保留的测试【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills导读本篇文章围绕当前仓库skills/engineering/tdd/下的 TDD测试驱动开发Skill 展开它是该仓库工程类 Skill 家族中负责以测试先行方式构建功能或修复缺陷的核心参考。阅读完本文你将掌握什么是值得保留的测试、测试应该放在哪种seam接缝上、必须规避的三大反模式实现耦合、同义反复、水平切片以及 red → green 循环的硬性规则同时会结合仓库内 tests.md 与 mocking.md 的完整代码示例理解 mock 的正确边界与可测试接口的设计方法。这套 Skill 由 Claude Code 加载属于仓库作者日常编码时被模型自动调用的工程技能见 skills/engineering/README.md。一、TDD Skill 的定位让循环产出值得保留的测试TDD 的核心是red → green 循环先写一个失败的测试red再写恰好能通过它的最少实现green。本仓库的tddSkill 并不是在重复这一常识而是充当让这个循环产出值得保留的测试的参考手册——它回答三个问题什么样的测试是好测试、测试放在哪里、以及循环中必须遵守哪些规则。该 Skill 的前言明确要求每个循环周期都要查阅这些规则而不是在循环结束后才补看。它被设计为过程性约束每次迭代cycle之前和之中都要对照执行。使用前的上下文约定在使用 TDD 前Skill 要求先探查代码库如果项目存在CONTEXT.md先阅读它使测试命名和接口词汇与项目的领域语言保持一致本项目根目录的 CONTEXT.md 正是这种领域词汇表的示范其中定义了 Issue tracker、Issue、Decision ticket、Triage role 等术语及应避免的替代词尊重你正在修改的代码区域内的 ADR架构决策记录。这一约定的意义在于测试名本身就是规格说明测试使用的词汇若与领域语言不一致测试的可读性与可检索性都会大打折扣。二、什么是好的测试通过公共接口验证行为Skill 给出的定义非常精炼测试通过公共接口验证行为而不是通过实现细节。代码可以彻底改变测试不应随之改变。一句判断标准是好的测试读起来像一份规格说明。例如测试名user can checkout with valid cart读者一看便知道系统具备何种能力由于它不关心内部结构因此能在重构中存活下来。好测试的特征与反例来自 tests.md仓库的 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); });好测试的特征清单测试用户/调用方关心的行为只使用公共 API能经受内部重构描述 WHAT做什么而非 HOW怎么做每个测试只含一个逻辑断言。与之相对的坏测试则直接与内部结构耦合// BAD: 测试实现细节 test(checkout calls paymentService.process, async () { const mockPayment jest.mock(paymentService); await checkout(cart, payment); expect(mockPayment.process).toHaveBeenCalledWith(cart.total); });坏测试的危险信号red flagsmock 内部协作者测试私有方法断言调用次数/调用顺序行为未变、仅做重构就导致测试破裂测试名描述 HOW 而非 WHAT绕过接口、通过外部手段如直接查数据库验证。绕过接口验证的典型反例tests.md 还专门举了一个绕过接口的对照// 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); });两者的差别在于坏测试验证的是数据库里插了一行实现细节好测试验证的是用户可被检索调用方真正关心的能力。这正是 Skill 主文档中通过旁路通道验证querying the database instead of using the interface这一反模式的实例化。三、Seam接缝测试应该放在哪里Seam是 TDD Skill 的核心概念借用自 Michael Feathers 的定义一处可以在不修改该处代码的情况下改变行为的位置也就是你观察行为时跨越的公共边界。测试必须放在 seam 上绝不面向内部实现。测试只在预先约定的 seam上进行Skill 给出了一条非常具体的工作纪律在写任何测试之前先写下将要测试的 seam 并和用户确认。任何测试都不能写在未经确认的 seam 上。原因是你不可能测试一切所以先约定 seam才能把有限的测试精力投放到关键路径和复杂逻辑上而不是铺满每一个边界情况。实际操作中应该直接问用户Whats the public interface, and which seams should we test?公共接口是什么我们该测哪些接缝与 codebase-design Skill 的协作当接口本身的形状存在争议时——比如模块应该多深、seam 应该放在哪里、接口应当暴露什么——Skill 要求调用codebase-design来获取共享词汇。codebase-design/SKILL.md 是该仓库中模块、接口、深度、seam、adapter、leverage、locality 等术语的唯一权威来源它是一个供参考的共享词汇表而不是需要运行的一次会话。其中与本主题最相关的两个原则是接口即测试面The interface is the test surface调用方与测试跨越的是同一个 seam如果你想越过接口去测试那说明模块的形状本身可能就不对内部 seam 与外部 seam一个深模块内部可以由许多小的、可 mock 的、可替换的部分组成但它们不属于接口的一部分——模块既可以有其实现私有的、供自身测试使用的内部 seam也可以有位于接口处的外部 seam。四、三大反模式Anti-patternsSkill 主文档列举了三种必须规避的反模式配合 tests.md 的代码示例可以完整理解1. 实现耦合Implementation-coupledmock 内部协作者、测试私有方法或通过旁路通道验证查数据库而不是用接口。判别信号重构后行为没变但测试却破裂了。// 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); });2. 同义反复Tautological断言用与实现相同的方式重新计算期望值导致测试按构造必然通过、永远不可能与代码产生分歧。Skill 主文档给出的典型例子是expect(add(a, b)).toBe(a b)以及手工用同样方式推导出来的快照常量断言等于自身。正确的做法是让期望值来自独立的事实来源已知正确的字面量、手工演算的例子、或规格说明。// GOOD: 期望值是独立的、已知正确的字面量 test(calculateTotal sums line items, () { expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15); });3. 水平切片Horizontal slicing先写完所有测试、再写全部实现。这样做的后果是成批的测试在验证想象出来的行为——你测的是事物的形状而不是面向用户的行为测试对真实变更变得不敏感并且你在理解实现之前就锁定了测试结构。正确的做法是垂直切片vertical slices一个测试 → 一段实现 → 重复。每个测试都是一枚曳光弹tracer bullet对上一轮循环教会你的东西做出响应。这与仓库中 implement Skill 的运作方式完全一致——implement明确要求在预先约定的 seam 上尽可能使用 /tdd。五、循环规则Rules of the loopSkill 主文档给出了三条硬性规则Red before green先红后绿先写失败的测试然后只写足够让它通过的最少代码。不要预判未来的测试不要添加投机性的功能。One slice at a time一次只切一片每个循环只处理一个 seam、一个测试、一个最小实现。Refactoring is not part of the loop重构不属于循环重构属于评审阶段对应仓库中的code-reviewSkill而不是 red → green 的实现循环。最后一条规则的意义在于明确职责边界TDD 循环负责让行为就位代码质量与规格符合度由 code-review/SKILL.md 在提交前承担——它从Standards是否符合代码规范 Fowler 坏味道基线与Spec是否忠实实现原始 issue/规格两个正交轴并行评审 diff且明确重构阶段就是其适用场景之一。在完整工作流中的位置从仓库文档可以梳理出 TDD 在完整工程流程中的位置to-tickets 把规格拆成曳光弹式的任务票据implement 依据规格或票据执行工作在预先约定的 seam 上驱动 /tdd并定期运行类型检查与单测文件、最后完整跑一遍测试套件完成后调用 /code-review 评审最后提交到当前分支。也就是说tdd Skill 是实现 → 评审链路中的实现环节核心重构与评审被显式地排除在循环之外、交给 code-review。六、何时 mock只在系统边界仓库中的 mocking.md 为 mock 划定了精确的适用范围。核心原则是只在系统边界system boundariesmock外部 API支付、邮件等数据库有时候——优先用测试数据库时间/随机性文件系统有时候。不要 mock你自己的类/模块内部协作者任何你完全掌控的东西。这与测试内部协作者的反模式互为表里如果你 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); }这与 codebase-design/SKILL.md 中接受依赖而不是创建依赖的可测试性设计原则完全同源Accept dependencies, dont create them。法则二优先使用 SDK 风格的接口而非通用 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), };SDK 风格带来的收益每个 mock 只返回一种特定形状shape测试 setup 中不需要条件逻辑更容易看出一个测试实际触发了哪些端点每个端点都有类型安全。七、Skill 的元数据与加载方式作为 Agent Skilltdd 的可发现性由 frontmatter 与代理配置共同决定skills/engineering/tdd/SKILL.md 的 frontmatter 声明了name: tdd与触发描述当用户想要以测试先行的方式构建功能或修复 bug、提到 red-green-refactor、或想要集成测试时使用skills/engineering/tdd/agents/openai.yaml 为其在 OpenAI 系 Agent 中定义了展示名称与短描述Test-driven red-green-refactor。从 skills/engineering/README.md 的分类看tdd 属于**模型自动调用Model-invoked**类技能它的触发短语被设计得足够丰富让模型在合适的时刻能够主动伸手够到它——例如用户提到 red-green-refactor、集成测试、或要求测试先行时。而 CHANGELOG.md 与 docs/engineering/tdd.md 则记录了该 Skill 在仓库中的演进与文档化副本供深入追踪历史变更。八、把规则落地的检查清单结合 Skill 主文档、tests.md 与 mocking.md在每一个 red → green 循环中可用如下清单自检维度检查项测试面是否只在预先与用户确认过的 seam上写测试行为 vs 细节测试是否只走公共接口、描述 WHAT 而非 HOW独立事实源期望值是否来自字面量/手工演算/规格而非重新计算切片方式是否一次一个测试、一个最小实现垂直切片循环顺序是否先红后绿、不添加投机性功能mock 边界是否只在系统边界 mock绝不 mock 自己掌控的模块接口形状依赖是否注入、外部操作是否为 SDK 风格的具体函数职责边界重构是否留给了 code-review 阶段而非循环内这套清单正是本仓库 tdd Skill 的全部纪律内核——它把red → green从一句口号变成了一套可逐条核验、可被 Agent 忠实执行的工程规范。延伸阅读主文档skills/engineering/tdd/SKILL.md好/坏测试示例skills/engineering/tdd/tests.mdMock 指南skills/engineering/tdd/mocking.md共享词汇seam、depth、interface 的权威定义skills/engineering/codebase-design/SKILL.md使用 /tdd 的上游 Skillskills/engineering/implement/SKILL.md承担重构与评审阶段的 Skillskills/engineering/code-review/SKILL.md领域词汇约定CONTEXT.md【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表