chore: import zh skill tdd

This commit is contained in:
wehub-skill-sync
2026-07-13 21:35:35 +08:00
commit 5c320871c4
7 changed files with 353 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# WeHub 来源说明
- Skill 名称:`tdd`
- 中文类目:通用红绿重构 TDD
- 上游仓库:`mattpocock__skills`
- 上游路径:`skills/engineering/tdd/SKILL.md`
- 上游链接:https://github.com/mattpocock/skills/blob/HEAD/skills/engineering/tdd/SKILL.md
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
- 原作者、版权和许可证信息以上游仓库为准
+109
View File
@@ -0,0 +1,109 @@
---
name: tdd
description: 测试驱动开发,采用红-绿-重构循环。当用户希望使用 TDD 构建功能或修复 Bug、提及"红-绿-重构"、需要集成测试,或要求测试优先开发时使用。
---
# 测试驱动开发
## 核心理念
**核心原则**:测试应通过公共接口验证行为,而非实现细节。代码可以彻底改变,但测试不应受影响。
**好的测试**是集成风格的:它们通过公共 API 执行真实的代码路径。它们描述的是系统**做什么**,而不是**怎么做**。好的测试读起来就像一份规格说明——"用户可以使用有效购物车结账"精确地告诉你存在什么能力。这类测试能经受重构,因为它们不关心内部结构。
**差的测试**与实现紧密耦合。它们模拟内部协作者、测试私有方法,或通过外部手段验证(例如直接查询数据库而不是通过接口)。警示信号:重构时测试失败,但行为并未改变。如果你重命名了一个内部函数而测试失败,那些测试测试的是实现,而非行为。
参见 [tests.md](tests.md) 获取示例,以及 [mocking.md](mocking.md) 了解模拟指南。
## 反模式:水平切片
**不要**先编写所有测试,再编写所有实现。这是"水平切片"——把 RED 阶段当作"编写所有测试",把 GREEN 阶段当作"编写所有代码"。
这会产生**糟糕的测试**
- 批量编写的测试测试的是**想象的**行为,而非**实际的**行为
- 你最终测试的是事物的**形状**(数据结构、函数签名),而非面向用户的行为
- 测试对真实变化不敏感——行为被破坏时它们通过,行为正常时它们却失败
- 你超出了自己的视野范围,在理解实现之前就确定了测试结构
**正确方法**:通过示踪弹头进行垂直切片。一个测试 → 一个实现 → 重复。每个测试都响应你在上一个周期中学到的东西。因为你刚刚编写了代码,所以确切知道什么行为重要以及如何验证它。
```
错误(水平切片):
RED test1, test2, test3, test4, test5
GREEN impl1, impl2, impl3, impl4, impl5
正确(垂直切片):
RED→GREEN test1→impl1
RED→GREEN test2→impl2
RED→GREEN test3→impl3
...
```
## 工作流程
### 1. 规划
在探索代码库时,使用项目的领域术语表,使测试名称和接口词汇与项目的语言保持一致,并尊重你所接触区域的架构决策记录(ADR)。
在编写任何代码之前:
- [ ] 与用户确认需要哪些接口变更
- [ ] 与用户确认要测试哪些行为(确定优先级)
- [ ] 识别适合[深度模块](deep-modules.md)的机会(小接口,深实现)
- [ ] 设计[可测试性](interface-design.md)接口
- [ ] 列出要测试的行为(而非实现步骤)
- [ ] 获得用户对计划的批准
询问:"公共接口应该是什么样子?哪些行为最重要需要测试?"
**你无法测试所有东西。** 与用户确认哪些行为最重要。将测试精力集中在关键路径和复杂逻辑上,而非每一个可能的边界情况。
### 2. 示踪弹头
编写一个测试,确认关于系统的一件事:
```
RED: 为第一个行为编写测试 → 测试失败
GREEN: 编写最简代码使其通过 → 测试通过
```
这就是你的示踪弹头——证明路径可以端到端工作。
### 3. 增量循环
对每个剩余行为:
```
RED: 编写下一个测试 → 失败
GREEN: 最简代码使其通过 → 通过
```
规则:
- 一次只写一个测试
- 只编写足够通过当前测试的代码
- 不要预先推测未来的测试
- 保持测试聚焦于可观察的行为
### 4. 重构
所有测试通过后,寻找[重构候选](refactoring.md)
- [ ] 提取重复代码
- [ ] 加深模块(将复杂度隐藏在简单接口之后)
- [ ] 在自然的情况下应用 SOLID 原则
- [ ] 考虑新代码揭示了关于现有代码的什么信息
- [ ] 每次重构后运行测试
**永远不要在 RED 状态下重构。** 先回到 GREEN。
## 每个周期的检查清单
```
[ ] 测试描述的是行为,而非实现
[ ] 测试只使用公共接口
[ ] 测试在内部重构后仍能存活
[ ] 代码针对此测试是最简的
[ ] 没有添加推测性的功能
```
+33
View File
@@ -0,0 +1,33 @@
# 深层模块
源自《软件设计的哲学》:
**深层模块** = 小接口 + 大量实现
```
┌─────────────────────┐
│ 小接口 │ ← 少方法、简单参数
├─────────────────────┤
│ │
│ │
│ 深层实现 │ ← 隐藏复杂逻辑
│ │
│ │
└─────────────────────┘
```
**浅层模块** = 大接口 + 少量实现(应避免)
```
┌─────────────────────────────────┐
│ 大接口 │ ← 多方法、复杂参数
├─────────────────────────────────┤
│ 薄实现 │ ← 仅做透传
└─────────────────────────────────┘
```
设计接口时,请思考:
- 能否减少方法的数量?
- 能否简化参数?
- 能否在内部隐藏更多复杂度?
+31
View File
@@ -0,0 +1,31 @@
# 面向可测试性的接口设计
好的接口能让测试变得自然:
1. **接收依赖,而非创建依赖**
```typescript
// 可测试
function processOrder(order, paymentGateway) {}
// 难测试
function processOrder(order) {
const gateway = new StripeGateway();
}
```
2. **返回结果,而非产生副作用**
```typescript
// 可测试
function calculateDiscount(cart): Discount {}
// 难测试
function applyDiscount(cart): void {
cart.total -= discount;
}
```
3. **缩小接触面**
- 方法越少 = 需要编写的测试越少
- 参数越少 = 测试设置越简单
+59
View File
@@ -0,0 +1,59 @@
# When to Mock
仅在**系统边界**处进行 Mock:
- 外部 API(支付、邮件等)
- 数据库(偶尔使用——优先用测试数据库)
- 时间/随机性
- 文件系统(偶尔使用)
不要 Mock
- 你自己的类/模块
- 内部协作组件
- 任何你能够控制的东西
## 为可 Mock 性而设计
在系统边界处,设计易于 Mock 的接口:
**1. 使用依赖注入**
将外部依赖传入,而非在内部创建:
```typescript
// 易于 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);
}
```
**2. 优先采用 SDK 风格的接口,而非通用 fetcher**
为每个外部操作创建特定函数,而非使用一个带条件逻辑的通用函数:
```typescript
// 好:每个函数可独立 Mock
const api = {
getUser: (id) => fetch(`/users/${id}`),
getOrders: (userId) => fetch(`/users/${userId}/orders`),
createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),
};
// 差:Mock 需要在内部实现条件逻辑
const api = {
fetch: (endpoint, options) => fetch(endpoint, options),
};
```
SDK 方式意味着:
- 每个 Mock 返回一种特定的数据结构
- 测试设置中无需条件逻辑
- 更容易看出某个测试覆盖了哪些端点
- 每个端点都有类型安全
+51
View File
@@ -0,0 +1,51 @@
# 重构候选项
TDD 循环结束后,留意以下内容:
- **重复代码** → 提取为函数/类
- **过长方法** → 拆分为私有辅助方法(测试保留在公共接口上)
- **过浅模块** → 合并或加深层次
- **特性依恋** → 将逻辑移至数据所在处
- **基本类型偏执** → 引入值对象
- **新代码揭示出的**现有代码存在的问题
Agent 工具可用的 agent 类型:
- **claude**:适用于任何不适合更特定 agent 的任务。未输入 agent 名称时 FleetView 的默认选项。(工具:*)
- **Explore**:只读搜索 agent,用于广泛发散搜索——当回答意味着需要扫描大量文件、目录或命名规范,而你只需要结论而不需要文件转储时使用。它读取的是摘录而非完整文件,因此用于定位代码,而非审查或审计代码。指定搜索广度:"medium" 表示适度探索,"very thorough" 表示多位置、多命名规范的搜索。(工具:除 Agent、Artifact、ExitPlanMode、Edit、Write、NotebookEdit 之外的所有工具)
- **general-purpose**:通用 agent,用于研究复杂问题、搜索代码以及执行多步骤任务。当你搜索某个关键字或文件,但不确定能否在前几次尝试中找到正确匹配时,使用此 agent 代为搜索。(工具:*)
- **Plan**:软件架构 agent,用于设计实施方案。当你需要规划某任务的实施策略时使用。返回分步骤方案,标识关键文件,并考虑架构上的权衡取舍。(工具:除 Agent、Artifact、ExitPlanMode、Edit、Write、NotebookEdit 之外的所有工具)
- **statusline-setup**:使用此 agent 配置用户的 Claude Code 状态栏设置。(工具:Read、Edit)
当你为独立工作启动多个 agent 时,通过单条消息中的多次工具调用一次性发送,使它们并发运行。
以下技能可供 Skill 工具使用:
- **deep-research**:深度研究框架——发散式网络搜索、抓取来源、对抗性验证声明、综合生成附有引用来源的研究报告。当用户需要对任何主题进行深入、多来源、经事实核查的研究时使用。调用前,先检查问题是否已足够具体以直接进行研究——如果定义不清(例如没有预算/用途/地区的"买什么车"),先追问 2-3 个澄清问题以缩小范围。然后将细化后的问题作为参数传入,将答案编织其中。
- **dataviz**:当你即将创建任何图表、图形、绘图、仪表盘或数据可视化时,无论输出介质是什么——HTML 或 React artifact、内联 SVG、任何库(matplotlib、plotly、d3、Recharts……)的绘图代码、将渲染并上传的图像/PNG,或是分享到 Slack 的图表——请在使用该技能前阅读说明。生成读起来像同一系统的可视化作品——优雅、可访问、在亮色和暗色模式下风格一致——使用品牌中性的占位调色板,你可以后续替换为自己的调色板。教授一种设计系统无关的方法:形式启发式、带可运行验证器的颜色公式、标记规范以及交互规则。已验证的默认调色板记录在 `references/palette.md` 中——可将该文件中的值替换为你品牌的值。触发词包括:"chart"、"graph"、"plot"、"data viz"、"visualization"、"dashboard"、"analytics"、"visualize data"、"categorical colors"、"sequential / diverging palette"、"stat tile"、"sparkline"、"heatmap"、"legend"、"axis"、"tooltip"、"chart colors"、"color by series"。
- **update-config**:使用此技能通过 settings.json 配置 Claude Code 框架。自动化行为("从现在起当 X 时"、"每次 X 时"、"每当 X 时"、"在 X 之前/之后")需要通过 settings.json 配置钩子——这些由框架执行,而非 Claude,因此记忆/偏好设置无法满足这些需求。也用于:权限("允许 X"、"添加权限"、"将权限移至")、环境变量("设置 X=Y")、钩子故障排查,或对 settings.json/settings.local.json 文件的任何更改。示例:"允许 npm 命令"、"将 bq 权限添加至全局设置"、"将权限移至用户设置"、"设置 DEBUG=true"、"当 claude 停止时显示 X"。对于 theme/model 等简单设置,建议使用 /config 命令。
- **keybindings-help**:当用户想要自定义键盘快捷键、重新绑定按键、添加组合键绑定或修改 ~/.claude/keybindings.json 时使用。示例:"重新绑定 ctrl+s"、"添加组合键快捷键"、"更改提交键"、"自定义快捷键"。
- **verify**:通过端到端执行并观察行为来验证代码更改是否确实按预期工作——驱动受影响的流程,而不仅仅是运行测试或类型检查。在进行非微小更改之前运行。如果 diff 仅涉及测试、文档或其他没有运行时表面可驱动的代码(产品源码的更改总是有运行时表面),则不要调用该技能——这种情况下没有什么可观察的。
- **code-review**:以指定努力级别审查当前 diff 的正确性错误以及复用/简化/效率清理(low/medium:较少但更确定的发现;high→max:更广泛的覆盖范围,可能包含不确定的发现)。使用 --comment 将发现作为内联 PR 评论发布,或使用 --fix 在审查后将修复应用到工作目录。
- **simplify**:审查已更改代码的复用、简化、效率和层次结构清理,然后应用修复。仅涉及质量——不寻找错误;请使用 /code-review 查找错误。
- **fewer-permission-prompts**:扫描你的对话记录以查找常见的只读 Bash 和 MCP 工具调用,然后向项目 .claude/settings.json 添加优先权限白名单,以减少权限提示。
- **loop**:按循环间隔运行某提示或斜杠命令(例如 /loop 5m /foo,默认为 10m)——当用户想要设置重复任务、轮询状态或按间隔重复运行某些操作时使用(例如"每 5 分钟检查一次部署"、"持续运行 /babysit-prs")。不要用于一次性任务。
- **claude-api**Claude API / Anthropic SDK 参考——模型 ID、定价、参数、流式传输、工具使用、MCP、agent、缓存、令牌计数、模型迁移。触发条件——在打开目标文件前阅读;不要因为"看起来是一行"就跳过——每当:提示中包含任何形式的 Claude/AnthropicClaude、Anthropic、Fable、Opus、Sonnet、Haiku、`anthropic``@anthropic-ai``claude-*``us.anthropic.*``[1m]`);用户询问关于 LLM 的问题(定价/模型选择/限制/缓存)——绝不要凭记忆回答;或者任务是 LLM 相关但未指明供应商(agent/MCP/工具定义/多 agent/RAG/LLM 评判/计算机使用;基于自然语言生成/总结/提取/分类/重写/对话;调试拒绝/截断/流式传输/工具调用/令牌)。仅在当前正在处理其他供应商时跳过(覆盖所有触发条件):查询中明确提及了 OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama;或者项目中 `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` 有命中(如果提示中未指明供应商,请先运行此 grep——不要直接读取文件)。
- **run**:启动并运行此项目的应用以查看更改效果。当被要求运行、启动或截图应用,或确认更改在实际应用中(而不仅仅是测试)能正常工作时使用。首先查找已涵盖启动应用的已有项目技能;否则回退到按项目类型(CLI、服务器、TUI、Electron、浏览器驱动、库)的内置模式。
- **init**:初始化一个包含代码库文档的新 CLAUDE.md 文件。
- **review**:审查 GitHub 拉取请求;对于当前工作目录的 diff,请使用 /code-review。
- **security-review**:完成对当前分支上待定更改的安全审查。作为后台任务运行。
- **babystar-prs**:运行已配置的按仓库拉取请求审查脚本并报告结果。
- **update-docs**:更新项目文档以反映代码的当前状态。当被要求"更新文档"或在进行了影响文档化设置/架构的更改后运行。
- **test**:此项目的测试工具——运行测试、检查覆盖率或修复失败的测试(通过分析失败原因,而非随机修改)。
- **mcp**:当用户提到"MCP"、"mcp"、"Claude Desktop 配置"、"MCP 服务器"、"mcp.json"或"mcp-config"时使用。不适用于其他工具调用模式。
- **commit**:帮助编写提交信息并提交更改。默认读取暂存更改;如果未暂存任何内容,则读取未暂存的已跟踪文件更改并暂存所有内容。使用 git diff --cached 或 git diff 直接验证将要提交的内容。在提交前与用户确认,除非已确认(如"commit this"或类似表述)。提交后,建议一个明确的推送命令。
- **claude-code**Claude Code / claude 命令的 CLI 使用参考——安装、标志、配置、认证、MCP、agent、CI/GitHub Actions、审查、斜杠命令、快捷键、设置、导出/导入、故障排除、FAQ、从 Cursor/Copilot/Windsurf/GitHub Copilot 迁移。在回答关于 Claude CodeCLI 工具 / claude 命令)的任何问题前阅读——配置、能力、MCP 设置、CI 集成、快捷键、设置、审查 PR、使用模式、FAQ 和故障排除。不适用于 Claude API(模型 ID、定价、SDK)——请使用 claude-api 获取。不适用于其他 AI 编码工具(Cursor、Copilot、Windsurf、Codeium)——声明你没有相关文档,并建议用户阅读 claude.ai/code 上的 /docs。如果用户询问的是他们自己的代码(错误、功能、重构)或非 ClaudeCode 工具,则跳过。
- **deploy**:部署到生产环境。仅在用户明确要求部署时使用。
- **pr**:创建或更新 GitHub 拉取请求。
- **pr-plan**:从 issue URL 创建 issue 分支,规划实施方案,并打开一个 PR。当用户提供了 GitHub issue URL 并要求实施并提交 PR 时运行。
- **brainlift**:通过创建参考文档来学习新的技能或工具。
- **publish**:发布到 npm、PyPI、Go 模块注册表、Homebrew 或其他包注册表。
- **doc**CLAUDE.md 中使用的 Markdown、编码模式和约定的快速参考。
- **statusline**:配置 Claude Code 状态栏。
- **fix**:审查、规划并修复当前的失败行为——运行 /code-review,分析错误与用户预期之间的差异,并应用针对性修复。
+61
View File
@@ -0,0 +1,61 @@
# 好的测试与糟糕的测试
## 好的测试
**集成风格**:通过真实接口进行测试,而非模拟内部组件。
```typescript
// 好:测试可观察的行为
test("用户可以使用有效购物车完成结账", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
```
特征:
- 测试用户/调用者关心的行为
- 仅使用公开 API
- 能够经受内部重构
- 描述「做什么」,而非「怎么做」
- 每个测试只有一个逻辑断言
## 糟糕的测试
**实现细节测试**:与内部结构耦合。
```typescript
// 差:测试实现细节
test("结账时调用 paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
```
危险信号:
- 模拟内部协作对象
- 测试私有方法
- 对调用次数/顺序进行断言
- 重构(未改变行为)时测试破裂
- 测试名称描述「怎么做」而非「做什么」
- 通过外部手段而非接口进行验证
```typescript
// 差:绕过接口进行验证
test("createUser 保存到数据库", async () => {
await createUser({ name: "Alice" });
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
expect(row).toBeDefined();
});
// 好:通过接口进行验证
test("createUser 使用户可被检索到", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
```