ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Sanity 单仓库 TDD 实践:红-绿-重构循环、曳光弹与行为测试

Sanity 单仓库 TDD 实践:红-绿-重构循环、曳光弹与行为测试 Sanity 单仓库 TDD 实践红-绿-重构循环、曳光弹与行为测试【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanitySanity 仓库通过.agents/skills/tdd/目录下的一套 Agent 技能文档完整定义了在其 monorepo 中实施测试驱动开发Test-Driven Development, TDD的方法论以红-绿-重构为核心循环用曳光弹tracer bullet策略做垂直切片坚持测试行为而非实现细节。读完本文你将掌握该技能包的四阶段工作流、每循环自检清单、Mock 边界判定、深模块与可测性接口设计原则并能结合仓库真实的 Vitest/Playwright 测试体系在类似大型 monorepo 中落地同一套 TDD 实践。技能定位与触发场景该技能的主文档 SKILL.md 通过 frontmatter 声明了自身元信息--- name: tdd description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions red-green-refactor, wants integration tests, or asks for test-first development. ---它属于 Sanity 仓库.agents/skills/技能家族中的一员同目录下还有 playwright-best-practices、code-review-and-quality 等供人类与 AI Agent 在构建新功能、修复 Bug、提到 red-green-refactor、要求集成测试或 test-first 开发时按此流程执行。主文档本身较精炼完整的实操指引由五个配套文档展开tests.md —— 好测试与坏测试的对照示例mocking.md —— Mock 使用边界与设计技巧deep-modules.md —— 深模块概念interface-design.md —— 面向可测性的接口设计refactoring.md —— 循环结束后的重构候选项。核心理念测试行为而非实现文档开宗明义给出核心原则Core principle: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldnt.测试应通过公共接口验证行为而非实现细节。代码可以彻底改变测试不应该变。好测试是集成风格的它们通过公共 API 驱动真实的代码路径描述系统做什么而非怎么做。一个好的测试读起来像一份规格说明——user can checkout with valid cart直接告诉你系统具备什么能力。这类测试能在重构中存活因为它们不关心内部结构。坏测试则与实现耦合mock 内部协作者、测试私有方法、或绕过接口从外部验证例如直接查数据库而不是用接口断言。文档给出的警告信号是你的测试在重构时挂掉了但行为并没有变。如果你重命名一个内部函数导致测试失败说明那些测试测的是实现不是行为。tests.md 给出了正反两组 TypeScript 示例// GOOD: Tests observable behavior 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每个测试一个逻辑断言反例一——mock 内部协作者// BAD: Tests implementation details 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 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: Verifies through interface test(createUser makes user retrievable, async () { const user await createUser({name: Alice}) const retrieved await getUser(user.id) expect(retrieved.name).toBe(Alice) })坏测试的红旗信号red flags包括mock 内部协作者、测试私有方法、断言调用次数/顺序、重构无行为变化时测试就挂、测试名描述 HOW 而非 WHAT、通过接口之外的手段验证结果。反模式水平切片这是整个技能包中被加粗强调的反模式DO NOT write all tests first, then all implementation.不要先写完所有测试再写所有实现。这被称为水平切片horizontal slicing——把 RED 理解为写完所有测试、把 GREEN 理解为写完所有代码。它产生的是crap tests垃圾测试具体危害有四点批量写出的测试测的是想象出来的行为不是真实行为你测的是事物的形状数据结构、函数签名而不是面向用户的行为测试对真实变化变得迟钝——行为坏掉时它们通过行为正常时它们失败你开得太快超出了车灯射程outrun your headlights——在还没理解实现之前就承诺了测试结构。正确的做法是用曳光弹做垂直切片一个测试 → 一个实现 → 重复。每个新测试都回应上一轮循环学到的东西。因为你刚刚写过代码你确切知道哪些行为重要、如何验证它WRONG (horizontal): RED: test1, test2, test3, test4, test5 GREEN: impl1, impl2, impl3, impl4, impl5 RIGHT (vertical): RED→GREEN: test1→impl1 RED→GREEN: test2→impl2 RED→GREEN: test3→impl3 ...四阶段工作流1. 规划Planning在写任何代码之前完成以下检查项与用户确认需要哪些接口变更与用户确认要测试哪些行为并做优先级排序识别深模块机会小接口、深实现为可测性设计接口列出要测试的行为注意是行为不是实现步骤获得用户对计划的批准规划阶段要向用户提问公共接口应该长什么样哪些行为最需要测试文档同时给出一个重要约束你不可能测试所有东西。必须与用户确认哪些行为最重要把测试精力集中在关键路径和复杂逻辑上而不是穷举每一个可能的边界情况。2. 曳光弹Tracer Bullet先写一个测试确认系统的一件事RED: Write test for first behavior → test fails GREEN: Write minimal code to pass → test passes这就是曳光弹——它证明这条路径端到端是通的为后续循环建立最小可行的红-绿基础。3. 增量循环Incremental Loop对每个剩余行为重复RED: Write next test → fails GREEN: Minimal code to pass → passes循环规则一次只写一个测试只写足以让当前测试通过的代码不预判未来的测试测试始终聚焦可观察行为4. 重构Refactor所有测试通过后按 refactoring.md 寻找重构候选项提取重复代码加深模块把复杂度移入简单接口之后在自然的地方应用 SOLID 原则思考新代码揭示了存量代码的哪些问题每一步重构后都运行测试并有一条铁律绝不在 RED 状态下重构——先拿到 GREEN。每循环自检清单每个红-绿循环结束后用主文档末尾的清单逐项自检[ ] Test describes behavior, not implementation [ ] Test uses public interface only [ ] Test would survive internal refactor [ ] Code is minimal for this test [ ] No speculative features added即测试描述行为而非实现、只使用公共接口、测试能存活内部重构、代码对该测试足够最小、没有加入投机性功能。何时 Mock只在系统边界mocking.md 的判定规则很简洁只在系统边界处 mock——外部 API支付、邮件等数据库有时——优先使用测试数据库时间/随机性文件系统有时明确不要mock 的对象你自己的类/模块、内部协作者、任何你能控制的东西。在系统边界处还要主动设计易于 mock的接口文档给出两条设计原则1. 使用依赖注入——把外部依赖传进来而不是在函数内部创建// Easy to mock function processPayment(order, paymentClient) { return paymentClient.charge(order.total) } // Hard to mock function processPayment(order) { const client new StripeClient(process.env.STRIPE_KEY) return client.charge(order.total) }2. 优先使用 SDK 风格接口而不是通用 fetcher——为每个外部操作建独立函数而不是一个带条件逻辑的通用函数// GOOD: Each function is independently mockable const api { getUser: (id) fetch(/users/${id}), getOrders: (userId) fetch(/users/${userId}/orders), createOrder: (data) fetch(/orders, {method: POST, body: data}), } // BAD: Mocking requires conditional logic inside the mock const api { fetch: (endpoint, options) fetch(endpoint, options), }SDK 方式的收益每个 mock 返回一个确定的形状、测试 setup 里没有条件逻辑、能一眼看出测试触达了哪些端点、每个端点都有类型安全。深模块小接口深实现deep-modules.md 引入《A Philosophy of Software Design》中的概念深模块 小接口 大量实现┌─────────────────────┐ │ Small Interface │ ← Few methods, simple params ├─────────────────────┤ │ │ │ │ │ Deep Implementation│ ← Complex logic hidden │ │ │ │ └─────────────────────┘浅模块 大接口 极少实现应避免┌─────────────────────────────────┐ │ Large Interface │ ← Many methods, complex params ├─────────────────────────────────┤ │ Thin Implementation │ ← Just passes through └─────────────────────────────────┘设计接口时的三个自检问题能否减少方法数量能否简化参数能否把更多复杂度藏到内部在 TDD 规划阶段识别深模块机会、在重构阶段加深模块是这条概念在该工作流中的两个落点。面向可测性的接口设计interface-design.md 给出三条让测试成为自然之事的接口设计原则接受依赖而不是创建依赖// Testable function processOrder(order, paymentGateway) {} // Hard to test function processOrder(order) { const gateway new StripeGateway() }返回结果而不是产生副作用// Testable function calculateDiscount(cart): Discount {} // Hard to test function applyDiscount(cart): void { cart.total - discount }小表面积——更少的方法意味着更少的测试更少的参数意味着更简单的测试 setup。这三条与 mocking.md 的依赖注入原则互为表里前者从设计视角约束接口形状后者从测试视角验证接口是否真的可被替换。循环结束后的重构候选项refactoring.md 列出了 TDD 循环结束后应扫描的代码坏味重复Duplication→ 提取函数/类长方法Long methods→ 拆成私有辅助函数测试保持在公共接口上浅模块Shallow modules→ 合并或加深依恋情结Feature envy→ 把逻辑移到数据所在处基本类型偏执Primitive obsession→ 引入值对象新代码揭示的存量代码问题→ 一并处理该实践在 Sanity 仓库中的落地佐证这套技能不是孤立的方法论文档而是与 Sanity 仓库真实的测试基建相互印证。以下从源码与配置层面给出证据。多项目 Vitest 编排。仓库根 vitest.config.mts 以projects数组注册了十余个测试项目packages/sanity、packages/sanity/schema、packages/sanity/validation、perf/bench、e2e等并配置了 v8 覆盖率reportOnFailure: true覆盖范围限定packages/**/src/**。这正对应 TDD 增量循环中每一步重构后运行测试的工程基础——测试按包分片可以只跑受影响的项目。AGENTS.md 中给 Agent 的标准操作是pnpm test # 全量 pnpm vitest run --projectsanity path # 单文件关键避免跑全量 pnpm test -- -u # 快照更新并要求pnpm build pnpm test——测试依赖编译产物。Mock 只发生在系统边界。AGENTS.md 明确单元测试在 jsdom 中运行、不需要任何真实认证需要 auth 上下文的组件在测试中使用createMockAuthStore。这正是 mocking.md 原则的工程化体现——认证这一系统边界被依赖注入式的 mock store 替代而组件自身的真实代码路径照常执行。仓库根配置甚至专门处理了 jsdom 与 Node 25 原生 Web Storage 的冲突vitest.config.mts 中用--no-experimental-webstorage让 jsdom 的实现生效可见测试基建本身也在持续做深实现、小接口式的维护。集成/端到端测试分层。与 TDD 技能好测试是集成风格的理念一致仓库把验证分成三层jsdom 单元测试Vitest无需认证、Vitest browser mode*.browser.test.tsx真实浏览器见 packages/sanity/vitest.config.mts 的 jsdom 环境排除规则、以及 Playwright E2Ee2e/需要 token主要跑 CI。AGENTS.md 的建议是大多数变更用pnpm build pnpm test即可验证只有需要视觉确认时才起 dev studio。行为优先的测试纪律。AGENTS.md 还体现了测试可观察行为的仓库级纪律jsdom 测试中禁止断言 vanilla-extract 类名或计算样式运行时样式被disableRuntimeStyles跳过要求改断言data-testid属性视觉/样式行为留给 browser mode 或 Playwright。这与 tests.md 中断言接口可见的结果而非内部机制是同一哲学在不同测试层级上的复现。边界 mock 的另一实例性能基准。perf/bench对构建后的 studio 做基准测试时面对的是本地 mock 的 Sanity API——fully hermetic, no tokens, no network见 AGENTS.md且 mock 契约测试被注册进根 Vitest projects作为bench mock 的漂移探测器在每个 PR 上运行vitest.config.mts 注释。这是在系统边界处 mock、并给 mock 本身写测试的完整示范。适用前提与限制该技能包面向在 Sanity monorepo 中工作的开发者与 AI Agent触发时机是 TDD 式的新功能/修 Bug 工作仓库整体的命令约定pnpm 版本、先 build 后 test、Conventional Commits PR 标题等以 AGENTS.md 为准确认接口与行为清单的规划步骤预设了与需求方人或 Agent 的对话方的交互纯自动化流水线场景需预先固定行为清单文中 TypeScript 示例为技能文档中的示意代码并非仓库内某函数的真实实现仓库内真实测试写法可参考 packages/sanity 与 packages/sanity/schema 下的*.test.ts文件。【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表