<!-- title: 造单测Skill：函数秒出可跑用例 -->

## 概述

写单元测试是开发中最常被跳过的环节——不是不会写，而是烦。本教程带你从零造一个 Claude Code 自定义 Skill：把一句 `/gen-tests utils.py calc_total` 变成一份"自动生成、自动跑绿、自动报覆盖率"的测试流水线。你将掌握 SKILL.md frontmatter、渐进式披露、动态上下文注入、参数传递和 skill-creator 评估这一整套 Skills 开发闭环，适合会基本命令行、想给团队沉淀测试规范的开发者。

## 前置条件

- **已安装 Claude Code v2.1.196 以上**（本文用到的 `${CLAUDE_PROJECT_DIR}`、`skill-creator` 描述调优依赖此版本及以上）。
- **Python 3.8+ 与 pytest**：实战案例以 Python 为例，命令可平移到 Jest/Vitest（文中给出对照）。
- **一个 git 项目**：Skill 可放个人级 `~/.claude/skills/` 跨项目复用，也可放项目级 `.claude/skills/` 随仓库走。
- **基础概念**：知道什么是 `~` 主目录、能在终端跑 `python3 --version`。

> 一句话定位：Skill 就是一个带 `SKILL.md` 的文件夹，目录名即命令名（如 `gen-tests/` → `/gen-tests`）。它的正文只在被调用时才进上下文，不像 CLAUDE.md 那样常驻——这正是它"省 token"的关键。

---

## 正文

### 第 1 步：理解 Skills 的三层加载机制（先想清楚再动手）

写 Skill 之前必须搞懂一句话：**Description 决定"何时用"，正文决定"怎么做"，支撑文件决定"做多深"。** 这三层对应官方说的渐进式披露（Progressive Disclosure）：

| 层 | 内容 | 何时进上下文 | 成本 |
| :-- | :-- | :-- | :-- |
| 第 1 层 | frontmatter 的 `description` + `when_to_use` | **始终在**（每个会话都加载，让 Claude 知道有这个能力） | 极低 |
| 第 2 层 | `SKILL.md` 正文 | **被调用时**才进，且整轮会话保留 | 中 |
| 第 3 层 | `reference.md`/`templates/` 等支撑文件 | 正文里显式引用、且确实需要时才读 | 按需 |

**关键推论**：description 要短（超过 1536 字符会被截断），且要把"最常见的触发场景"放第一句；而那些"边界条件清单""框架模板"这种大段参考材料，**绝对不要塞进正文**，要拆到支撑文件里——否则每轮都在烧 token。

**为什么单测适合做成 Skill？** 因为它天然是"重复粘贴同一套检查清单"的活：每次都要想正常值、空值、边界、异常……把它固化成 Skill，团队每个人生成的测试就有了统一的"底色"。

### 第 2 步：创建 Skill 骨架

先决定放哪。个人级（全项目可用）最省事：

```bash
mkdir -p ~/.claude/skills/gen-tests/{templates,scripts}
```

得到结构：

```text
gen-tests/
├── SKILL.md          # 入口，必需
├── reference.md      # 边界条件清单（第 3 层，按需）
├── templates/
│   └── pytest_template.py
└── scripts/
    └── extract_fn.py # 抽取目标函数源码的辅助脚本
```

> **预期结果**：目录建好后无需重启——官方支持 Live change detection，会话内增删 Skill 即时生效。只有"会话启动时还不存在的顶层 skills 目录"才需要重启 Claude Code。

> **常见问题**：建在项目级 `.claude/skills/` 时，首次使用会弹"workspace trust"对话框——因为带 `allowed-tools` 的项目级 Skill 能给自己授权，必须人工确认后才生效。这正是安全机制。

### 第 3 步：写 SKILL.md——frontmatter 是灵魂

把下面内容存为 `~/.claude/skills/gen-tests/SKILL.md`：

```yaml
---
description: 为指定源文件中的函数生成并运行单元测试。当用户说"给这个函数写测试""补一下单测""提升这块覆盖率""gen-tests"时使用。
argument-hint: [源文件路径] [函数名]
allowed-tools: Bash(pytest *) Bash(python3 *) Read Write Edit
---

# 单元测试生成器

为 `$0` 中的函数 `$1` 生成高质量单元测试，跑到全部通过为止。

## 环境（自动探测）
!`python3 -m pytest --version 2>/dev/null || echo "pytest 未安装，先用 pip install pytest 安装"`
!`ls tests/ 2>/dev/null || echo "无 tests 目录，将在仓库根创建"`

## 执行步骤
1. 读取 `$0`，定位函数 `$1`。可用 `python3 ${CLAUDE_SKILL_DIR}/scripts/extract_fn.py $0 $1` 抽取源码。
2. 照 `${CLAUDE_SKILL_DIR}/templates/pytest_template.py` 的结构写测试。
3. 对照 `${CLAUDE_SKILL_DIR}/reference.md` 的边界条件清单逐条覆盖：正常值、空/零值、边界、非法输入与异常、浮点精度。
4. 测试写入 `tests/test_$1.py`，命名 `test_<函数>_<场景>`。
5. 运行 `pytest tests/test_$1.py -v`，失败就分析、补用例或修正预期，循环到全绿。
6. 输出：新增用例数、覆盖的场景、尚未覆盖的点。
```

逐字段拆解——这是本教程最该记的部分：

- **`description`**（推荐写）：Claude 据此判断"要不要自动触发"。**把口语触发词写进去**（"给这个函数写测试""补一下单测"），命中率会显著提高。它和 `when_to_use` 拼在一起，在 Skill 列表里被截断到 **1536 字符**，所以好钢用在刀刃上。
- **`argument-hint`**：斜杠菜单自动补全时的提示，写 `[源文件路径] [函数名]` 让人一看就懂要传几个参数。
- **`allowed-tools`**：Skill 激活期间，列出的工具**免逐次确认**。这里放了 `pytest`/`python3`/`Read`/`Write`/`Edit`——生成测试要读源码、写文件、跑测试。**注意：它只授权不限制**，没列的工具照常能用，权限设置才是底线。
- **`$0`、`$1`**：位置参数。`/gen-tests utils.py calc_total` → `$0=utils.py`，`$1=calc_total`。
- **`${CLAUDE_SKILL_DIR}`**：Skill 自己的目录，**无论 Skill 装在个人级还是项目级都能正确解析**，是引用自带脚本/模板的"绝对路径替身"。

> **本可省略但值得知道的字段**：
> - `disable-model-invocation: true`——禁止 Claude 自动跑，只允许你手动 `/gen-tests`（像 `/deploy` 这种有副作用的才加；单测无害，这里不加）。
> - `user-invocable: false`——反过来，只让 Claude 自动用、不进 `/` 菜单（适合"背景知识"类 Skill）。
> - `context: fork` + `agent: Explore`——让 Skill 在隔离子代理里跑，不污染主对话（适合重型研究任务）。

> **常见问题：Skill 不触发？** 90% 是 description 没写或 YAML 写挂了。YAML 格式错时 Claude Code 会用空元数据加载，`/gen-tests` 仍能用但没了自动触发。跑 `claude --debug` 能看到解析报错。

### 第 4 步：补支撑文件——把"重料"拆出去（渐进式披露的精髓）

正文保持精简，大段参考材料进单独文件，正文只在需要时"指路"。

**`reference.md`（边界条件清单）**：

```markdown
# 单元测试边界条件清单（生成时逐条自检）

## 输入维度
- **正常值**：典型业务输入，断言正确输出
- **空值/零值**：空列表、空串、0、None、空 dict
- **单元素**：列表只有一项
- **边界**：数值的最小/最大、列表长度 0/1/N
- **非法输入**：负数、超范围、错误类型 → 断言抛预期异常
- **重复/乱序**：重复元素、未排序输入
- **浮点**：用 pytest.approx，绝不用 ==

## 命名
test_<被测函数>_<被测场景>，如 test_calc_total_折扣为1时返回0

## 断言异常
with pytest.raises(ValueError):
    calc_total(items, discount=1.5)
```

**`templates/pytest_template.py`（结构模板）**：

```python
# 复制此结构，替换 {module} {Func} {func}
import pytest
from {module} import {func}

class Test{Func}:
    def test_{func}_正常值(self):
        # given / when / then
        assert result == expected

    def test_{func}_空输入(self):
        assert {func}([]) == 0

    def test_{func}_非法输入抛异常(self):
        with pytest.raises(ValueError):
            {func}("bad")
```

**`scripts/extract_fn.py`（抽取目标函数源码，纯标准库）**：

```python
#!/usr/bin/env python3
"""从源文件抽取指定函数源码，供生成测试时参考。
用法: extract_fn.py <文件> [函数名]"""
import ast, sys, pathlib

def main():
    path = pathlib.Path(sys.argv[1])
    lines = path.read_text(encoding="utf-8").splitlines()
    tree = ast.parse("\n".join(lines))
    want = {sys.argv[2]} if len(sys.argv) > 2 else None
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if want is None or node.name in want:
                seg = "\n".join(lines[node.lineno-1: node.end_lineno])
                print(f"# --- {node.name} ({path}:{node.lineno}) ---\n{seg}\n")

if __name__ == "__main__":
    main()
```

> **预期结果**：三份文件就位后，正文始终轻量；当 Claude 真要写测试时，才把 `reference.md` 的清单和模板"拉进"上下文。这就是第 1 步那张表里第 3 层的落地。

> **常见问题**：SKILL.md 官方建议控制在 **500 行以内**。如果你的 reference 越写越长，考虑再拆——比如 `reference-python.md`、`reference-jest.md`，正文按 `$1` 语言动态指路。

### 第 5 步：动态上下文注入——让 Skill"睁眼看到"真实环境

正文里这两行是点睛之笔：

````markdown
## 环境（自动探测）
!`python3 -m pytest --version 2>/dev/null || echo "pytest 未安装，先用 pip install pytest 安装"`
!`ls tests/ 2>/dev/null || echo "无 tests 目录，将在仓库根创建"`
````

`!` 反引号语法会在 **Skill 内容发给 Claude 之前**先执行命令，把输出原地替换进去。于是 Claude 看到的不是"请检查 pytest 是否安装"这种指令，而是 pytest 是否安装的**事实**——决策更准。

> **三条铁律**：
> 1. `!` 必须在**行首或空格之后**，紧跟其它字符（如 `KEY=!\`cmd\``）会被当字面量、不执行。
> 2. 注入是**一次性预处理**，命令输出是纯文本，不会再被扫描出新的 `!`。
> 3. 多行命令用 ` ```! ` 围栏块，而不是行内 `!\``。

> **常见问题**：企业想统一禁掉 Skill 的命令执行？在 settings.json 设 `"disableSkillShellExecution": true`，所有 `!` 会被替换成"[shell command execution disabled by policy]"。注意它只影响 user/project/plugin 来源的 Skill，bundled 和 managed 的不受影响。

### 第 6 步：两种方式触发并验证

进任意含 `utils.py` 的项目，跑 `claude`，然后：

**方式 A——自动触发**（靠 description 命中）：

```text
帮 calc_total 这个函数补一下单测
```

**方式 B——显式调用**（最稳）：

```text
/gen-tests utils.py calc_total
```

两种都能跑通。预期 Claude 会：读 `utils.py` → 照模板生成 `tests/test_calc_total.py` → 跑 `pytest` → 失败就修 → 全绿后报告。

> **常见问题：Skill 用着用着"失灵"了？** 多半是**自动压缩**（auto-compaction）把它从上下文挤掉了。压缩时 Claude Code 会给每个已调用 Skill 保留前 5000 token、合计 25000 token 预算，从最近调用的开始保。调了很多 Skill 后，早期的可能整条被丢。**对策**：压缩后重新 `/gen-tests ...` 一次，把完整内容再灌回去。

### 第 7 步：用 skill-creator 量化"它到底好不好"

"能触发"不等于"做得对"。官方推荐装 skill-creator 插件做对照评估：

```text
/plugin install skill-creator@claude-plugins-official
/reload-plugins
```

装不上就先 `/plugin marketplace update claude-plugins-official` 刷新市场。然后：

```text
用 skill-creator 评估我的 gen-tests skill
```

它会：① 收集真实 prompt 存进 `evals/evals.json`；② 每个 case 起一个干净子代理分别跑"开 Skill"和"关 Skill"；③ 对断言逐条打分写进 `grading.json`；④ 汇总出 `benchmark.json`——**通过率提升了多少 vs 多花了多少 token/时间**；⑤ 还能在两个 Skill 版本间盲测 A/B，确认你的改动是真改进再提交；⑥ 自动生成 should-trigger / should-not-trigger prompt，测 description 命中率并建议改写。

> **这是判断"该不该提交"的硬指标**：不要凭感觉改 Skill，用 benchmark 说话。

---

## 实战案例：给 `calc_total` 一键造出 5 个能跑的用例

在被测项目根放一个 `utils.py`：

```python
# utils.py
def calc_total(items, discount=0.0):
    """计算商品总价并应用折扣。"""
    if not items:
        return 0
    if discount < 0 or discount > 1:
        raise ValueError("折扣必须在 0 到 1 之间")
    subtotal = sum(i["price"] * i["qty"] for i in items)
    return round(subtotal * (1 - discount), 2)
```

在项目根跑：

```text
/gen-tests utils.py calc_total
```

Claude 读完函数、对照边界清单后，会生成 `tests/test_calc_total.py`：

```python
import pytest
from utils import calc_total

def test_calc_total_空列表返回0():
    assert calc_total([]) == 0

def test_calc_total_正常求和():
    items = [{"price": 10, "qty": 2}, {"price": 5, "qty": 1}]
    assert calc_total(items) == 25

def test_calc_total_折扣正确():
    items = [{"price": 100, "qty": 1}]
    assert calc_total(items, discount=0.2) == 80.0

def test_calc_total_折扣超范围抛异常():
    with pytest.raises(ValueError):
        calc_total([{"price": 1, "qty": 1}], discount=1.5)

def test_calc_total_浮点精度():
    items = [{"price": 0.1, "qty": 3}]
    assert calc_total(items, discount=0.1) == pytest.approx(0.27)
```

接着自动 `pytest -v`，预期 5 passed。Claude 末尾汇报：

```text
✅ 新增 5 个用例，覆盖：正常求和、折扣、空输入、非法输入、浮点精度
📊 未覆盖建议：超大列表性能、qty 为 0 的商品项
🔧 查覆盖率：pytest --cov=utils tests/test_calc_total.py
```

**改造成 Jest？** 把 `templates/pytest_template.py` 换成 `jest_template.test.js`，`reference.md` 里 `pytest.raises` 换成 `expect(fn).toThrow()`，`allowed-tools` 里 `pytest` 换成 `npx jest *`，正文步骤 5 的命令改成 `npx jest`。Skill 骨架完全复用——这正是 Skill 比一次性 prompt 强的地方：**固化一次，反复套用**。

---

## 工作流程图

```mermaid
flowchart TD
    A["用户: /gen-tests utils.py calc_total"] --> B["Claude 读 SKILL.md frontmatter"]
    B --> C{"动态上下文注入 预处理"}
    C --> D["探测 pytest 版本与 tests 目录"]
    C --> E["按需加载 templates 与 reference.md"]
    D --> F["读 utils.py 定位 calc_total"]
    E --> F
    F --> G["照模板生成 test_calc_total.py"]
    G --> H["运行 pytest -v"]
    H --> I{"全部通过?"}
    I -->|否| J["分析失败 补边界用例"]
    J --> H
    I -->|是| K["汇报用例数 覆盖场景 剩余建议"]
    K --> L["skill-creator 跑 benchmark 量化改进"]
```

---

## 延伸阅读

- **Skill 不触发的诊断手册**：官方 `/debug-your-config` 流程，一步步排查 Skill 为何不出现或不触发。
- **跨产品的 Skill 最佳实践**：platform.claude.com 上的 "Skill authoring best practices"，写法在 Claude.ai、Claude Code 各产品通用。
- **子代理 + Skill 组合**：`context: fork` 让 Skill 在隔离上下文跑，配合 `.claude/agents/` 自定义代理可做更重的研究型 Skill。
- **Plugin 打包分发**：给 Skill 文件夹加 `.claude-plugin/plugin.json`，就能把 Skill 连同 hooks、MCP、agents 一起作为插件发布。

---

## 训练营适用性评估

| 维度 | 评估 |
| :-- | :-- |
| **适合的训练营类型** | 企业研发提效营、Python/全栈实战营、AI 工程化营、测试工程专项营。不适合纯零基础（需懂 Python 与终端）。 |
| **前置门槛** | 中低。会用终端、写过简单 Python 函数即可；不要求懂 LLM 内部。 |
| **建议教学时长** | 授课 + 跟做 **2.5–3 小时**：30 分钟讲三层加载机制，90 分钟动手造 gen-tests（7 步全跑通），30 分钟做 skill-creator 评估与改造。 |
| **学员产出** | 一个可复用、随仓库走的测试 Skill，以及一套可迁移到任意"重复检查清单"场景的方法论。 |
| **进阶练习建议** | ① 加 `context: fork` 让测试生成在子代理隔离运行；② 扩展到 Jest/Vitest 双模板；③ 用 `paths: "**/*.py"` 限定只在编辑 Python 文件时自动触发；④ 把"未覆盖建议"接上真实 `pytest --cov` 输出做闭环。 |

---

## 参考来源

1. [Extend Claude with skills — Claude Code 官方文档](https://code.claude.com/docs/en/skills) — Skill 的目录结构、frontmatter 字段表、动态上下文注入、子代理运行、skill-creator 评估的权威说明，本文核心机制均出自此处。
2. [Claude Skills Explained: Use Custom Skills on Claude Code — Analytics Vidhya](https://www.analyticsvidhya.com/blog/2026/03/claude-skills-custom-skills-on-claude-code/) — 从零构建、配置、使用自定义 Skill 的实操教程，社区实战视角。
3. [Claude Code Skills: The Complete Guide (2026) — DesignRevision](https://designrevision.com/blog/claude-code-skills) — "Skill 是带 SKILL.md 的文件夹、按需加载专业知识"的完整 2026 指南，覆盖渐进式披露理念。
4. [skill-creator plugin — Anthropic 官方插件仓库](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/skill-creator) — 自动化 Skill 评估循环（test cases、grading、benchmark、A/B、description 调优）的工具实现。
5. [Claude Skills: The Complete 2026 Guide — Build, Install & Use — BuildFastWithAI](https://www.buildfastwithai.com/blogs/claude-skills-complete-guide-2026) — Skill 架构原理与关键实践的综合指南，可作为体系化补充阅读。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
