本教程面向已经能跑通 Claude Code、但还没亲手写过 Skill 的开发者。你将从零搭建一个测试骨架生成 Skill(test-scaffold):它会扫描项目里的源文件,提取函数与方法签名,并自动生成对应语言的单测样板文件。学完你将掌握 Skill 的目录约定、SKILL.md frontmatter 写法、脚本与引用文件的渐进式披露、触发调试技巧,以及一套可直接放进 .claude/skills/ 运行的完整示例。
ast,无需第三方依赖);.py 文件的目录用于实战(没有也无妨,文末会给出可运行的样例);核心心智模型:Skill 不是插件、不是 API,而是一个带说明书的文件夹。Claude 在会话启动时扫描
~/.claude/skills/、.claude/skills/与插件目录,只把每个 Skill 的name+description(约 30–50 tokens)载入系统提示;真正用到时才读取正文与脚本。这套"渐进式披露"是后续所有设计的出发点。
Skill 的最小可用形态只需一个 SKILL.md。但实战中,把"说明书、脚本、参考"分层存放,既利于复用也契合渐进式披露:
mkdir -p .claude/skills/test-scaffold/{scripts,references}
cd .claude/skills/test-scaffold
最终目录长这样:
.claude/skills/test-scaffold/
├── SKILL.md # 必需:核心说明书
├── scripts/
│ └── extract_symbols.py # 用 AST 提取函数/方法签名
└── references/
├── jest_template.md # JS 端测试模板(按需加载)
└── pytest_template.md # Python 端测试模板(按需加载)
预期结果:目录创建成功,tree .claude/skills/test-scaffold 能看到上述结构。
常见问题:放在 .claude/skills/(项目级)还是 ~/.claude/skills/(个人级)?答案取决于是否要团队共享——项目级会随 git 提交分发给同事,个人级只对你全局生效。本教程用项目级,便于团队复用。
整个 Skill 能否被触发,几乎全看 description 这一个字段。Claude 用语义匹配把用户意图与所有已索引 Skill 的描述比对:模糊的"分析数据文件"几乎不会触发,而"扫描 Python 源文件、提取函数与方法、生成 pytest 测试样板"则命中率极高。
name 的硬约束:全小写字母、数字、连字符,≤64 字符,且不能含 anthropic、claude 等保留词或 XML 标签。description 上限 1024 字符,实践中 20 字符以下基本无效。无效 YAML 会导致静默加载失败——Skill 直接不进索引,连报错都没有。
新建 .claude/skills/test-scaffold/SKILL.md:
---
name: test-scaffold
description: 为 Python/JavaScript 源文件生成单元测试骨架。扫描指定目录,提取函数与方法签名,输出对应语言的测试样板(pytest / Jest)。当用户要求"生成测试""补单测""写测试样板""为 xxx 模块补测试"或处理缺少测试覆盖的代码时触发。支持指定框架与目标目录。
---
# 测试骨架生成 Skill
## 何时使用
当用户希望为新写或已有的函数/类快速产出可运行的单测样板时使用。
## 工作流程
1. 运行 `python scripts/extract_symbols.py <源文件或目录> --lang python` 得到符号清单(JSON)。
2. 根据目标语言读取对应模板:Python 读 `references/pytest_template.md`,JS 读 `references/jest_template.md`。
3. 把每个公共函数/方法渲染成一个测试用例:一个正常路径、一个边界值、一个异常路径。
4. 写入 `tests/test_<模块名>.py`(或 `.test.<name>.js`),避免覆盖已有文件。
5. 运行生成的测试,确认能 collect 通过(即使断言待补)。
## 约定
- 跳过以 `_` 开头的私有符号,除非用户明确要求。
- 不修改被测源码,只新增测试文件。
- 若目标测试文件已存在,在其末尾追加,并打印提示。
预期结果:保存后,SKILL.md 的 frontmatter 合法,重启 Claude Code 后该 Skill 进入索引。
常见问题与解决:
| 现象 | 根因 | 解决 |
|---|---|---|
| 装好了但从不触发 | description 只写"做什么"没写"何时用" |
补上触发词与"当用户要求…时"句式 |
| Skill 索引里消失 | YAML 解析失败或 name 含非法字符 |
用在线 YAML 校验器检查;确认 --- 恰好出现 |
| 改了文件没生效 | 改动只在启动时扫描 | 重启 Claude Code |
| 不想被自动触发,只想手动调用 | 默认语义匹配太活跃 | 加 disable-model-invocation: true,只能 /test-scaffold 显式调用 |
Skill 的黄金法则:能用代码确定完成的,绝不靠模型生成。脚本代码本身不进入上下文,只有它的输出消耗 token——所以一个 300 行的提取脚本,最终只往对话里塞一份几十行的 JSON 清单。
新建 scripts/extract_symbols.py,用 Python 标准库 ast 提取函数与方法签名(零依赖):
#!/usr/bin/env python3
# extract_symbols.py — 从 Python 源文件提取公共函数/方法签名
import ast, json, sys, argparse
from pathlib import Path
def is_public(name: str) -> bool:
return not name.startswith("_")
def extract(path: Path) -> list[dict]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
symbols = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not is_public(node.name):
continue
args = [a.arg for a in node.args.args]
ret = ast.unparse(node.returns) if node.returns else None
symbols.append({
"name": node.name,
"qualname": node.name,
"args": args,
"returns": ret,
"is_method": isinstance(getattr(node, "parent", None), ast.ClassDef),
"line": node.lineno,
})
elif isinstance(node, ast.ClassDef) and is_public(node.name):
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and is_public(item.name):
symbols.append({
"name": item.name,
"qualname": f"{node.name}.{item.name}",
"args": [a.arg for a in item.args.args],
"returns": ast.unparse(item.returns) if item.returns else None,
"is_method": True,
"class": node.name,
"line": item.lineno,
})
return symbols
def main():
ap = argparse.ArgumentParser()
ap.add_argument("target", help="源文件或目录")
ap.add_argument("--lang", default="python")
args = ap.parse_args()
root = Path(args.target)
files = [root] if root.is_file() else list(root.rglob("*.py"))
files = [f for f in files if "test_" not in f.name] # 跳过已有测试
out = {}
for f in files:
try:
syms = extract(f)
if syms:
out[str(f)] = syms
except SyntaxError as e:
print(f"[skip] {f}: {e}", file=sys.stderr)
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
预期结果:对一个含函数与类的 .py 文件运行,输出形如:
{
"src/pricing.py": [
{"name": "compute_total", "qualname": "compute_total", "args": ["items", "tax_rate"], "returns": "float", "is_method": false, "line": 12},
{"name": "apply_discount", "qualname": "Cart.apply_discount", "args": ["self", "code"], "returns": null, "is_method": true, "class": "Cart", "line": 40}
]
}
常见问题:
- 拿不到方法:ast.walk 会把类内方法当普通函数遍历,所以脚本额外在 ClassDef 内显式收集,并标注 is_method/class,方便模板渲染时正确加 self。
- 想支持 JS/TS:Python 的 ast 不解析 JS。两条路——要么调用项目已有的 ts-morph/@babel/parser(JS 脚本版),要么对 JS 退化为正则提取 function foo( 与 foo(...) =>。后者不精确,但作为"骨架起点"足够,并在 Skill 说明里标注其局限。
把两种语言的模板拆进 references/,而不是全塞进 SKILL.md。原因来自渐进式披露第三阶段:引用文件只在被读取时才进上下文。一个只做 Python 测试的调用,就不必为 Jest 模板付 token。
references/pytest_template.md 示例:
# pytest 测试样板渲染规则
对每个符号 `s`,生成:
```python
import pytest
from <module> import <qualname 的类/函数>
def test_<name>_happy_path():
# 正常路径:给出合法输入,断言返回值
result = <call with sensible defaults>
assert result is not None
def test_<name>_boundary():
# 边界值:空集合 / 0 / 极值
pass
def test_<name>_error():
# 异常路径:非法输入应抛出预期异常
with pytest.raises(Exception):
<call with bad input>
cart = Cart(); cart.apply_discount("X")。# TODO: 填入合理入参 标注,绝不编造业务数值。src.pricing)。`references/jest_template.md` 同理,给出 `describe/it` 骨架。
**预期结果**:`SKILL.md` 保持精简(指南建议 < 500 行),大段模板按需加载,token 占用最小化。
**常见问题**:
- **模板要不要内联进 SKILL.md?** 短模板可以内联;超过几十行或存在多语言分支时,外置成引用文件更划算——这正是渐进式披露的价值。
- **模型不主动读 references/**:在 `SKILL.md` 的工作流里**显式写明**"读取 `references/pytest_template.md`",Claude 才会按指令加载。说明书要像给新人的操作手册,每一步可执行、可验证。
## 第五步:注册、触发与调试
1. **确认放置位置**:项目级 `.claude/skills/test-scaffold/SKILL.md`。
2. **重启 Claude Code**:发现扫描只在初始化进行,改完文件必须重启。
3. **验证索引**:用一句贴近触发词的自然语言试触发——
> 帮我为 `src/pricing.py` 补一份 pytest 测试样板。
预期 Claude 读取 Skill → 运行 `extract_symbols.py` → 读模板 → 生成 `tests/test_pricing.py` → 跑 `pytest --collect-only` 确认可收集。
4. **不触发时的排查清单**(生产环境高频坑):
- `description` 是否同时含"做什么"和"何时用"?✓
- 是否含用户真实会用到的词("补单测""测试样板")?✓
- 改动后是否重启?✓
- 实在不行,**显式点名**:`用 test-scaffold 这个 skill 给 pricing.py 生成测试`——这是最可靠的兜底。
5. **可选:收紧权限**。这个 Skill 只读源码、只写测试目录,可在 frontmatter 加:
```yaml
allowed-tools: ["Read", "Write", "Bash", "Glob", "Grep"]
```
作为纵深防御(但别只依赖它,仍需审阅脚本本身)。
## 实战案例:为一个订单计价模块生成测试
假设 `src/pricing.py`:
```python
def compute_total(items: list[float], tax_rate: float) -> float:
return sum(items) * (1 + tax_rate)
class Cart:
def __init__(self):
self.items: list[float] = []
def add(self, price: float) -> None:
self.items.append(price)
def apply_discount(self, code: str) -> float:
# 业务规则略
...
在项目根目录对 Claude 说:"用 test-scaffold 给 src/pricing.py 补测试。" 期望产出 tests/test_pricing.py:
import pytest
from src.pricing import compute_total, Cart
def test_compute_total_happy_path():
result = compute_total([10.0, 20.0], 0.1)
assert result == pytest.approx(33.0)
def test_compute_total_boundary():
assert compute_total([], 0.1) == 0.0 # 空集合
def test_compute_total_error():
with pytest.raises(TypeError):
compute_total(None, 0.1)
class TestCart:
def setup_method(self):
self.cart = Cart()
def test_apply_discount_happy_path(self):
# TODO: 填入合法折扣码与预期金额
assert isinstance(self.cart.apply_discount("VIP10"), float)
随后 Claude 执行 pytest --collect-only,看到 7 tests collected 即说明骨架可运行、断言待补。整个过程中,提取脚本的 60 行源码没有占用任何上下文 token——只有那份 JSON 清单与最终测试文件进入对话。这就是脚本化 Skill 的成本优势。
flowchart TD
A[会话启动] --> B[扫描 .claude/skills/*]
B --> C[仅加载 name+description\n约30-50 tokens/Skill]
C --> D{用户: 给 pricing.py 补测试}
D --> E[语义匹配命中 test-scaffold]
E --> F[加载 SKILL.md 正文]
F --> G[运行 scripts/extract_symbols.py]
G --> H[脚本输出 JSON 符号清单\n脚本源码不进上下文]
H --> I[按需读取 references/pytest_template.md]
I --> J[渲染并写入 tests/test_pricing.py]
J --> K[pytest --collect-only 验证]
K --> L[7 tests collected ✓]
SKILL.md,大段模板/多语言分支放 references/ 按需读。https://code.claude.com/docs/en/skillshttps://support.claude.com/en/articles/12512198https://github.com/gregpriday/claude-code-docs/blob/main/claude-code-skills.mdhttps://claudskills.com/learn/claude-code-skill-frontmatter-fields/https://mcpmarket.com/tools/skills/claude-skill-development-template-7allowed-tools 权限收紧 + 多语言模板 + CI 中调用"进阶环节。[!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 等场景全覆盖。
