首页 › tutorials › 造单测Skill:函数秒出可跑用例

概述

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

前置条件

一句话定位: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 骨架

先决定放哪。个人级(全项目可用)最省事:

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 能给自己授权,必须人工确认后才生效。这正是安全机制。

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

把下面内容存为 ~/.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. 输出:新增用例数、覆盖的场景、尚未覆盖的点。

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

本可省略但值得知道的字段: - 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(边界条件清单):

# 单元测试边界条件清单(生成时逐条自检)

## 输入维度
- **正常值**:典型业务输入,断言正确输出
- **空值/零值**:空列表、空串、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 语言动态指路。

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

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

## 环境(自动探测)
!`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 命中):

帮 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 ... 一次,把完整内容再灌回去。

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

"能触发"不等于"做得对"。官方推荐装 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 量化改进"]

延伸阅读


训练营适用性评估

维度 评估
适合的训练营类型 企业研发提效营、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 官方文档 — Skill 的目录结构、frontmatter 字段表、动态上下文注入、子代理运行、skill-creator 评估的权威说明,本文核心机制均出自此处。
  2. Claude Skills Explained: Use Custom Skills on Claude Code — Analytics Vidhya — 从零构建、配置、使用自定义 Skill 的实操教程,社区实战视角。
  3. Claude Code Skills: The Complete Guide (2026) — DesignRevision — "Skill 是带 SKILL.md 的文件夹、按需加载专业知识"的完整 2026 指南,覆盖渐进式披露理念。
  4. skill-creator plugin — Anthropic 官方插件仓库 — 自动化 Skill 评估循环(test cases、grading、benchmark、A/B、description 调优)的工具实现。
  5. Claude Skills: The Complete 2026 Guide — Build, Install & Use — BuildFastWithAI — 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学通信知识星球海报-扫码加入