写单元测试是开发中最常被跳过的环节——不是不会写,而是烦。本教程带你从零造一个 Claude Code 自定义 Skill:把一句 /gen-tests utils.py calc_total 变成一份"自动生成、自动跑绿、自动报覆盖率"的测试流水线。你将掌握 SKILL.md frontmatter、渐进式披露、动态上下文注入、参数传递和 skill-creator 评估这一整套 Skills 开发闭环,适合会基本命令行、想给团队沉淀测试规范的开发者。
${CLAUDE_PROJECT_DIR}、skill-creator 描述调优依赖此版本及以上)。~/.claude/skills/ 跨项目复用,也可放项目级 .claude/skills/ 随仓库走。~ 主目录、能在终端跑 python3 --version。一句话定位:Skill 就是一个带
SKILL.md的文件夹,目录名即命令名(如gen-tests/→/gen-tests)。它的正文只在被调用时才进上下文,不像 CLAUDE.md 那样常驻——这正是它"省 token"的关键。
写 Skill 之前必须搞懂一句话:Description 决定"何时用",正文决定"怎么做",支撑文件决定"做多深"。 这三层对应官方说的渐进式披露(Progressive Disclosure):
| 层 | 内容 | 何时进上下文 | 成本 |
|---|---|---|---|
| 第 1 层 | frontmatter 的 description + when_to_use |
始终在(每个会话都加载,让 Claude 知道有这个能力) | 极低 |
| 第 2 层 | SKILL.md 正文 |
被调用时才进,且整轮会话保留 | 中 |
| 第 3 层 | reference.md/templates/ 等支撑文件 |
正文里显式引用、且确实需要时才读 | 按需 |
关键推论:description 要短(超过 1536 字符会被截断),且要把"最常见的触发场景"放第一句;而那些"边界条件清单""框架模板"这种大段参考材料,绝对不要塞进正文,要拆到支撑文件里——否则每轮都在烧 token。
为什么单测适合做成 Skill? 因为它天然是"重复粘贴同一套检查清单"的活:每次都要想正常值、空值、边界、异常……把它固化成 Skill,团队每个人生成的测试就有了统一的"底色"。
先决定放哪。个人级(全项目可用)最省事:
mkdir -p ~/.claude/skills/gen-tests/{templates,scripts}
得到结构:
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 能给自己授权,必须人工确认后才生效。这正是安全机制。
把下面内容存为 ~/.claude/skills/gen-tests/SKILL.md:
---
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能看到解析报错。
正文保持精简,大段参考材料进单独文件,正文只在需要时"指路"。
reference.md(边界条件清单):
# 单元测试边界条件清单(生成时逐条自检)
## 输入维度
- **正常值**:典型业务输入,断言正确输出
- **空值/零值**:空列表、空串、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(结构模板):
# 复制此结构,替换 {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(抽取目标函数源码,纯标准库):
#!/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语言动态指路。
正文里这两行是点睛之笔:
## 环境(自动探测)
!`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 的不受影响。
进任意含 utils.py 的项目,跑 claude,然后:
方式 A——自动触发(靠 description 命中):
帮 calc_total 这个函数补一下单测
方式 B——显式调用(最稳):
/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 ...一次,把完整内容再灌回去。
"能触发"不等于"做得对"。官方推荐装 skill-creator 插件做对照评估:
/plugin install skill-creator@claude-plugins-official
/reload-plugins
装不上就先 /plugin marketplace update claude-plugins-official 刷新市场。然后:
用 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:
# 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)
在项目根跑:
/gen-tests utils.py calc_total
Claude 读完函数、对照边界清单后,会生成 tests/test_calc_total.py:
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 末尾汇报:
✅ 新增 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 强的地方:固化一次,反复套用。
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 量化改进"]
/debug-your-config 流程,一步步排查 Skill 为何不出现或不触发。context: fork 让 Skill 在隔离上下文跑,配合 .claude/agents/ 自定义代理可做更重的研究型 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 输出做闭环。 |
[!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 等场景全覆盖。
