<!-- title: 造API契约测试Skill：规范秒变用例 -->

## 概述

本教程带你从零构建一个名为 `api-contract-test` 的 Claude Code Skill，它读取任意 OpenAPI/Swagger 规范文件，自动为每个端点生成正向、边界、负向三类契约测试用例，并产出可直接比对实现的测试报告。学完你将掌握 SKILL.md 的 YAML frontmatter 规范、渐进式披露的三阶段加载机制、脚本与指令的协作编排，以及如何用脚本确定性运算替代模型 token 生成。适用人群：有 REST API 开发经验、希望用 AI 把枯燥测试编写自动化的后端工程师与 SDET。

## 前置条件

- Claude Code v1.0+（自带 Skill 支持，无需额外安装）
- 一个含 OpenAPI 3.x 规范（`openapi.yaml`/`openapi.json`）的工程目录
- 基础的 Python 3 与 YAML 常识；脚本依赖 `pyyaml`，Claude Code 沙箱可按需 `pip install`

---

## 一、为什么需要一个契约测试 Skill

测试 API 最让人头疼的不是"会不会写"，而是"写不完"。一个中等规模的服务动辄三五十个端点，每个端点都要覆盖正常路径、必填校验、类型校验、越界、未授权、资源不存在——手动写一遍要几天，而规范一旦微调，测试又得跟着改。更隐蔽的是**契约漂移**：实现偷偷返回了规范里没声明的状态码，或者悄悄改了字段名，测试还全绿，线上却在裸奔。

OpenAPI 规范本身就是一份机器可读的契约：它声明了每个端点接受什么参数、返回什么状态码、响应体长什么样。既然契约已经写在文件里，用例就不该手写——它们应该从规范里"长"出来。这正是 Claude Code Skill 的理想场景：把"解析规范→枚举端点→按策略派生用例→比对实现"这套确定性流程固化成可复用的能力，模型只在需要语义判断时介入。

把它做成 Skill 而非一次性脚本，关键收益有三：

1. **模型自主触发**——你说"给这个 API 补点测试"，Skill 自动激活，你不必记命令；
2. **渐进式披露**——端点解析、用例生成、断言执行拆成独立脚本与参考文件，按需加载，不占上下文；
3. **团队可复用**——Skill 放进 `.claude/skills/` 提交到 Git，全组共享同一套测试口径。

## 二、设计：用渐进式披露切分 Skill

Skill 的核心设计原则是**三阶段加载**。会话启动时，Claude 只读取每个 Skill 的 `name` 和 `description` 两个字段（约 30–50 token），据此判断要不要激活；激活后才加载 SKILL.md 正文；正文里引用的脚本和参考文件，则在真正执行时才被读取——而脚本的**代码本身从不进入上下文**，只有它的输出消耗 token。这意味着你可以塞进一个上千行的解析器，却只为最终的 JSON 结果买单。

按这个原则，我们的 Skill 目录结构如下：

```
.claude/skills/api-contract-test/
├── SKILL.md                    # 核心指令（必需）
├── scripts/
│   ├── parse_spec.py           # 解析 OpenAPI，提取端点契约
│   ├── gen_cases.py            # 按策略生成测试用例
│   └── run_contract.py         # 对真实服务发起请求并断言
├── references/
│   ├── case_strategies.md      # 用例设计策略（按需加载）
│   └── assertion_rules.md      # 状态码与 schema 断言规则
└── templates/
    └── test_report.md          # 报告骨架
```

`SKILL.md` 始终加载，保持精简（建议 500 行内）；`scripts/` 的代码确定性执行、不入上下文；`references/` 只在模型需要决策（比如"这个端点该怎么设计负向用例"）时才被读进来。

## 三、步骤一：编写 SKILL.md 的 frontmatter

frontmatter 是 Skill 的"身份证"，也是模型决定是否激活的唯一依据。两个字段必填、校验严格：`name` 限 64 字符、只能用小写字母/数字/连字符，且不能含 `anthropic`、`claude` 等保留词或 XML 标签；`description` 限 1024 字符，**必须同时说明"做什么"和"何时用"**——这是 Skill 能否被可靠触发的命脉。模糊描述如"分析接口"几乎不会激活；有效描述要塞入用户真实会说的触发词。

新建文件 `.claude/skills/api-contract-test/SKILL.md`：

```markdown
---
name: api-contract-test
description: |
  Generate contract test cases from an OpenAPI/Swagger specification.
  Reads openapi.yaml/openapi.json, enumerates every endpoint, and
  produces positive, boundary, and negative test cases with expected
  status codes and response schemas. Use when the user asks to test an
  API, generate API test cases, write contract tests, validate endpoints
  against an OpenAPI/swagger spec, or detect contract drift between the
  implementation and its spec. Also use when onboarded to an unfamiliar
  API and wanting a quick contract-level smoke test.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
---

# API 契约测试生成器

（正文见下一节）
```

`allowed-tools` 是可选的工具白名单：写明它能让权限提示更少、行为更可控。注意它**约束的是模型可用工具集的提示，并非硬性沙箱**——真正收紧执行仍需配合权限模式（如 `permissions` 里的 `deny` 规则）。如果 Skill 需要默认全部工具，直接省略该字段。

**常见问题**：装好了却不触发？九成是 description 没塞够触发词。把"contract test""契约测试""OpenAPI""端点校验"这些用户高频词都写进去，比纠结指令文笔重要得多。

## 四、步骤二：编写 SKILL.md 正文指令

正文是给模型的工作手册。好的正文是**声明式、分步骤、带边界**的——告诉模型在什么条件下做什么、用什么脚本、产出什么格式，而不是泛泛而谈"请测试这个 API"。续写 SKILL.md：

```markdown
# API 契约测试生成器

从 OpenAPI/Swagger 规范自动生成契约测试用例，覆盖 positive / boundary / negative 三类。

## 工作流程（严格按序执行）

1. **定位规范文件**：用 Glob 搜索 `**/openapi.{yaml,yml,json}` 与 `**/swagger.{yaml,yml,json}`。
   若多个命中或零命中，向用户确认路径，不要猜测。

2. **解析规范**：
   `python3 scripts/parse_spec.py <spec-path> > endpoints.json`
   得到所有端点的契约清单。若报 `ModuleNotFoundError: yaml`，先 `pip install pyyaml`。

3. **生成用例**：
   `python3 scripts/gen_cases.py endpoints.json > test_cases.json`
   得到结构化用例列表。

4. **可选——对真实服务跑契约**：仅当用户给出可访问的 base URL 时执行
   `python3 scripts/run_contract.py test_cases.json <base-url> > results.json`。
   对 POST/PUT/DELETE 等写操作，执行前必须向用户确认，默认只生成不执行。

5. **产出报告**：按 `templates/test_report.md` 渲染，按端点分组，
   标注 pass / fail / skip，并对 fail 用例标注实际状态码与预期的差异（契约漂移）。

## 用例设计原则（详见 references/case_strategies.md）

- **positive**：规范声明的每个 2xx 响应，至少一条用例
- **boundary**：每个 required 参数缺失一次 → 预期 400；类型不符、越界值
- **negative**：无认证 → 401；无权限 → 403；不存在资源 → 404

## 输出约定

每条用例必须含字段：`endpoint`、`category`、`name`、`expected_status`。
报告必须按 endpoint 分组，并在末尾给出统计：通过数 / 失败数 / 跳过数 / 漂移数。

## 边界（不要做的事）

- 不修改任何业务代码，只产出测试文件与报告
- 不主动对生产环境发起写请求
- 规范含跨文件 `$ref` 时，提示用户先合并为单文件再运行
```

注意正文刻意把"何时调用哪个脚本、产出什么中间文件"写死——这把模型从"自由发挥"拉回"确定性流水线"，避免它每次用不同方式拆解规范导致结果不可复现。

## 五、步骤三：编写解析脚本 parse_spec.py

确定性的事交给脚本，不交给 token。这个脚本提取每个端点的方法、路径、参数、请求体、声明的响应码，并解析 `$ref` 引用：

```python
#!/usr/bin/env python3
"""解析 OpenAPI 规范，提取可测试的端点契约。"""
import json, sys, yaml

def load_spec(path):
    with open(path, encoding='utf-8') as f:
        return yaml.safe_load(f) if path.endswith(('.yaml', '.yml')) else json.load(f)

def resolve_ref(spec, ref):
    """解析形如 #/components/schemas/Pet 的 $ref。"""
    if not ref or not ref.startswith('#/'):
        return None
    node = spec
    for part in ref.lstrip('#/').split('/'):
        node = node.get(part, {}) if isinstance(node, dict) else None
        if node is None:
            return None
    return node

def extract_endpoints(spec):
    endpoints = []
    for path, item in spec.get('paths', {}).items():
        path_params = item.get('parameters', [])
        for method, op in item.items():
            if method.lower() not in ('get', 'post', 'put', 'patch', 'delete'):
                continue
            params = list(path_params) + op.get('parameters', [])
            endpoints.append({
                'id': f"{method.upper()} {path}",
                'method': method.upper(),
                'path': path,
                'operationId': op.get('operationId', f"{method}_{path}"),
                'summary': op.get('summary', ''),
                'tags': op.get('tags', []),
                'parameters': params,
                'requestBody': op.get('requestBody'),
                'responses': list(op.get('responses', {}).keys()),
            })
    return endpoints

if __name__ == '__main__':
    spec = load_spec(sys.argv[1])
    print(json.dumps({
        'title': spec.get('info', {}).get('title', ''),
        'version': spec.get('info', {}).get('version', ''),
        'endpoint_count': len(extract_endpoints(spec)),
        'endpoints': extract_endpoints(spec),
    }, ensure_ascii=False, indent=2))
```

**预期输出**（针对一个含 `/pets` 的 Petstore 规范）：

```json
{
  "title": "Petstore",
  "version": "1.0.0",
  "endpoint_count": 2,
  "endpoints": [
    { "id": "GET /pets", "method": "GET", "responses": ["200", "400"] },
    { "id": "POST /pets", "method": "POST", "responses": ["201", "405"] }
  ]
}
```

**常见问题**：`$ref` 解析返回 None？多半是引用跨了文件（形如 `external.yaml#/...`），脚本只处理文件内引用。按 SKILL.md 的边界约定，提示用户先把多文件合并成单文件。

## 六、步骤四：编写用例生成脚本 gen_cases.py

这是 Skill 的"大脑"，把端点契约翻译成具体用例。核心策略：声明成功的响应码各派一条正向用例；每个 required 参数缺失派一条边界用例（预期 400）；若规范声明了 401，派一条未授权负向用例。

```python
#!/usr/bin/env python3
"""按 positive/boundary/negative 策略生成契约测试用例。"""
import json, sys

def gen_for_endpoint(ep):
    cases = []
    # positive：每个 2xx 响应一条
    for code in ep['responses']:
        if str(code).startswith('2'):
            cases.append({'endpoint': ep['id'], 'category': 'positive',
                          'name': f"{ep['id']} 正常返回 {code}",
                          'expected_status': int(code)})
    # boundary：每个 required 参数缺失一条
    for p in ep.get('parameters', []):
        if p.get('required'):
            cases.append({'endpoint': ep['id'], 'category': 'boundary',
                          'name': f"{ep['id']} 缺少必填参数 {p['name']}",
                          'expected_status': 400})
    # negative：规范声明 401 → 无凭证；声明 404 → 资源不存在
    if '401' in ep['responses']:
        cases.append({'endpoint': ep['id'], 'category': 'negative',
                      'name': f"{ep['id']} 无认证访问",
                      'expected_status': 401})
    if '404' in ep['responses']:
        cases.append({'endpoint': ep['id'], 'category': 'negative',
                      'name': f"{ep['id']} 访问不存在的资源",
                      'expected_status': 404})
    return cases

if __name__ == '__main__':
    data = json.load(open(sys.argv[1]))
    all_cases = []
    for ep in data['endpoints']:
        all_cases.extend(gen_for_endpoint(ep))
    print(json.dumps({'case_count': len(all_cases), 'cases': all_cases},
                     ensure_ascii=False, indent=2))
```

**预期输出**（接上例）：

```json
{ "case_count": 4, "cases": [
  {"endpoint":"GET /pets","category":"positive","name":"GET /pets 正常返回 200","expected_status":200},
  {"endpoint":"GET /pets","category":"boundary","name":"GET /pets 缺少必填参数 limit","expected_status":400},
  {"endpoint":"POST /pets","category":"positive","name":"POST /pets 正常返回 201","expected_status":201},
  {"endpoint":"POST /pets","category":"negative","name":"POST /pets 无认证访问","expected_status":401}
]}
```

**常见问题**：生成的用例太少？检查规范的 `required: true` 是否真的标在每个参数上——很多团队写规范时偷懒不标 required，导致边界用例为零。这种"规范本身不严谨"恰是契约测试的价值所在，应在报告里反推提醒。

## 七、步骤五：编写契约执行脚本 run_contract.py（可选）

生成用例后，若想直接验证实现是否达标，需要一个发起请求并比对的脚本。它读 `test_cases.json`，对每条用例的 endpoint 发请求，比对实际状态码与 `expected_status`：

```python
#!/usr/bin/env python3
"""对真实服务逐条执行契约用例并断言状态码。"""
import json, sys, urllib.request, urllib.error

def run(cases, base_url):
    results = []
    for c in cases['cases']:
        method, path = c['endpoint'].split(' ', 1)
        url = base_url.rstrip('/') + path
        req = urllib.request.Request(url, method=method)
        try:
            with urllib.request.urlopen(req, timeout=5) as resp:
                actual = resp.getcode()
        except urllib.error.HTTPError as e:
            actual = e.code
        except Exception as e:
            results.append({**c, 'actual_status': None, 'verdict': 'skip', 'error': str(e)})
            continue
        verdict = 'pass' if actual == c['expected_status'] else 'fail'
        results.append({**c, 'actual_status': actual, 'verdict': verdict,
                        'drift': actual != c['expected_status']})
    return results

if __name__ == '__main__':
    cases = json.load(open(sys.argv[1]))
    base_url = sys.argv[2]
    results = run(cases, base_url)
    passed = sum(1 for r in results if r['verdict'] == 'pass')
    print(json.dumps({'total': len(results), 'passed': passed,
                      'failed': sum(1 for r in results if r['verdict'] == 'fail'),
                      'skipped': sum(1 for r in results if r['verdict'] == 'skip'),
                      'results': results}, ensure_ascii=False, indent=2))
```

用标准库 `urllib` 而非 `requests`，是为了**零依赖**——沙箱里不必额外安装。`fail` 且 `drift=true` 的用例就是契约漂移点，是报告里最该被高亮的部分。

## 八、步骤六：补齐参考文件与报告模板

`references/case_strategies.md` 写策略详解（何时派边界、负向用例如何选资源 ID），`references/assertion_rules.md` 写状态码与 schema 断言细则。它们按需加载——模型遇到"这个 PUT 端点该测哪些边界"时才读，平时不占 token。

`templates/test_report.md` 给出报告骨架：

```markdown
# API 契约测试报告

- 规范：{{title}} v{{version}}
- 端点数：{{endpoint_count}} | 用例数：{{case_count}}
- 通过 {{passed}} / 失败 {{failed}} / 跳过 {{skipped}}

## 契约漂移（FAIL）

| 端点 | 用例 | 预期 | 实际 |
|------|------|------|------|
{{#drifts}}| {{endpoint}} | {{name}} | {{expected_status}} | {{actual_status}} |
{{/drifts}}

## 逐端点明细
（按 endpoint 分组列出每条用例与 verdict）
```

## 九、实战案例：给一个微型 Petstore 跑通全流程

在工程根目录放一份 `openapi.yaml`（节选）：

```yaml
openapi: 3.0.0
info: { title: Petstore, version: "1.0.0" }
paths:
  /pets:
    get:
      summary: 列出宠物
      parameters:
        - { name: limit, in: query, required: true, schema: { type: integer } }
      responses:
        "200": { description: OK }
        "400": { description: 缺参数 }
    post:
      summary: 新建宠物
      responses:
        "201": { description: 创建成功 }
        "401": { description: 未认证 }
```

然后在 Claude Code 里直接用自然语言驱动：

> 给这个项目的 API 补一套契约测试，规范在 openapi.yaml。

模型会：① Glob 命中 `openapi.yaml` → ② 跑 `parse_spec.py` 得 2 个端点 → ③ 跑 `gen_cases.py` 得 4 条用例 → ④ 因你未给 base URL，跳过执行 → ⑤ 按 `test_report.md` 产出报告，并指出"`GET /pets` 声明了 400 但参数 limit 标了 required，已派边界用例覆盖"。

整个过程你只说了一句话，模型自主激活了 Skill、选对了脚本、按既定流程产出。这就是 Skill 把"专家流程"封装为"一句话能力"的威力。

## 工作流程图

```mermaid
flowchart TD
    A["用户: 给这个API补契约测试"] --> B{"Skill是否激活?"}
    B -->|description命中触发词| C["加载 SKILL.md 正文"]
    B -->|未命中| Z["不激活,按普通对话处理"]
    C --> D["Glob 定位 openapi.yaml"]
    D --> E["执行 scripts/parse_spec.py<br/>提取端点契约 (不入上下文)"]
    E --> F["endpoints.json"]
    F --> G["执行 scripts/gen_cases.py<br/>派生 positive/boundary/negative"]
    G --> H["test_cases.json"]
    H --> I{"用户提供 base URL?"}
    I -->|是且确认写操作| J["执行 run_contract.py<br/>断言状态码,标漂移"]
    I -->|否| K["跳过执行"]
    J --> L["results.json"]
    K --> L
    L --> M["按 templates/test_report.md<br/>渲染分组报告"]
    M --> N["输出报告 + 漂移清单"]
    style B fill:#fef3c7
    style E fill:#dbeafe
    style G fill:#dbeafe
    style J fill:#dbeafe
```

图中蓝色节点是脚本执行——它们承担确定性运算，代码不入上下文，只有 JSON 输出消耗 token。黄色节点是模型唯一的语义判断点（激活与否）。把重活压到脚本、把轻判断留给模型，是写好 Skill的黄金分工。

## 调试与优化要点

- **触发不稳定**：用 debug 模式启动 Claude Code 可看到 Skill 加载日志，确认 `api-contract-test` 是否进入索引；未进入通常是 YAML 格式错误（`---` 必须恰好成对）或 `name` 含非法字符。
- **改了 Skill 没生效**：发现扫描只在会话启动时进行，修改后必须重启 Claude Code 才会重新发现——这是最常踩的坑。
- **上下文吃紧**：若 SKILL.md 正文超 500 行，把策略细则全部下沉到 `references/`，正文只留工作流程与边界。
- **提示词缓存友好**：Skill 集合保持稳定能让系统提示命中 5 分钟缓存；频繁增删 Skill 会使缓存失效、推高成本。
- **安全**：Skill 会以你的权限执行任意代码，只装可信来源的 Skill；本例的 `run_contract.py` 默认不碰写操作，正是出于这个考虑。

## 延伸阅读

- Anthropic 官方 Skills 文档 `code.claude.com/docs/en/skills`——frontmatter 与发现机制的权威说明
- Schemathesis——从 OpenAPI 规范做属性测试（property-based）的开源利器，可作为本 Skill 的执行后端
- CATS（Baeldung 教程）——另一款契约自动测试工具，提供 HTML 报告范式
- Dredd——将 OpenAPI 规范转成可执行契约测试的经典工具，适合对比用例设计思路
- `gregpriday/claude-code-docs`——社区维护的 Claude Code Skills 技术深扒，含三阶段加载细节

## 训练营适用性评估

- **适用类型**：后端工程师训练营、SDET/测试自动化训练营、AI 辅助研发训练营（中级）。前置需 REST API 与 OpenAPI 基础，不适合纯零基础。
- **教学时长**：建议 3 小时。1 小时讲 Skill 架构与渐进式披露原理，1.5 小时动手搭建四个脚本与 SKILL.md 并跑通 Petstore 案例，0.5 小时做契约漂移实战与调优讨论。
- **可拓展作业**：① 给生成的用例加上 JSON Schema 响应断言；② 接入 Schemathesis 做属性测试；③ 把 Skill 改造为支持 GraphQL schema 的版本；④ 用 `skill-creator` 跑 eval，量化 description 触发准确率。
- **能力闭环**：本教程从"读规范"到"生成用例"到"比对实现"，完整覆盖契约测试全链路，学员可带走一个能直接投入自己项目的 Skill。

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — Anthropic 官方 Skills 文档，frontmatter 字段与发现机制的权威来源
2. [Claude Code Skills: Complete Technical Deep-Dive](https://github.com/gregpriday/claude-code-docs/blob/main/claude-code-skills.md) — 三阶段渐进式加载、无状态模型调用、token 效率等技术细节
3. [SKILL.md frontmatter reference — ClaudSkills](https://claudskills.com/learn/skill-md-frontmatter-reference/) — name/description/allowed-tools 等 YAML 字段的逐项解析
4. [Automated Testing for OpenAPI Endpoints Using CATS — Baeldung](https://www.baeldung.com/cats-openapi-automated-testing) — 从 OpenAPI 规范自动生成并执行契约测试的工具范式
5. [Enforcing API Correctness: Automated Contract Testing with OpenAPI](https://dev.to/r3d_cr0wn/enforcing-api-correctness-automated-contract-testing-with-openapi-and-dredd-2212) — 契约测试三层结构（结构校验、模糊测试、契约断言）的设计思路

> [!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)
