我用 AI 把 Swagger 变成 500 条 pytest 用例,回归从 3 天到 3 小时

上个月我把订单中台的回归测试从 Postman 里拽了出来。之前 87 个接口,每次发版前手工点,3 天起步,点到后面人都麻了。现在 CI 上跑 500 多条 pytest,全量回归 3 小时出头,MR 只跑 smoke 的话 20 分钟。

核心不是哪个神器,而是把 Swagger 当成数据源,让 AI 补测试场景,再让 pytest 去干重复劳动。下面是我实际落地的过程,代码是删减过的,但骨架能直接抄。

先别碰 AI,把 Swagger 弄干净

我一开始也想着直接拿 Swagger 喂给模型,结果生成一堆不存在的字段。后来发现,问题不在 AI,在 Swagger 本身。

我们要求每个接口必须满足:

  • OpenAPI 3.0,别拿 Swagger 2.0 凑合;

  • operationId 唯一,后面用例 ID 靠它;

  • 每个 schema 字段尽量有 example

  • 必填字段、枚举、最大最小长度写清楚;

  • 错误响应别只写个 400,把业务错误码也列出来。

校验命令我放在 pre-commit 里:

复制代码
openapi-spec-validator openapi.yaml

这一步很枯燥,但省不掉。Swagger 越像合同,后面生成的用例越像回事。

把接口抽成"用例原材料"

先写个脚本,把 OpenAPI 里的 operation 拉平。我不直接生成测试文件,先生成一份 YAML,方便人工审核。

复制代码
# tools/extract_ops.py
import yaml

HTTP_METHODS = {"get", "post", "put", "patch", "delete"}

def extract_operations(spec):
    for path, methods in spec["paths"].items():
        for method, op in methods.items():
            if method not in HTTP_METHODS:
                continue
            yield {
                "id": op.get("operationId") or f"{method}_{path}".replace("/", "_"),
                "method": method.upper(),
                "path": path,
                "summary": op.get("summary", ""),
                "description": op.get("description", ""),
                "parameters": op.get("parameters", []),
                "requestBody": op.get("requestBody", {}),
                "responses": op.get("responses", {}),
            }

if __name__ == "__main__":
    with open("openapi.yaml", encoding="utf-8") as f:
        spec = yaml.safe_load(f)
    ops = list(extract_operations(spec))
    with open("cases/raw_ops.yaml", "w", encoding="utf-8") as f:
        yaml.safe_dump(ops, f, allow_unicode=True, sort_keys=False)
    print(f"抽到 {len(ops)} 个 operation")

我们 87 个接口,抽出来 87 条。但这只是原材料,不是用例。

让 AI 干它擅长的:想场景

接口测试最烦的不是发请求,是想"还要测什么"。正常流谁都会写,缺参、类型错、边界值、权限、幂等,这些才费脑子。

我把每个 operation 的 JSON 发给模型,要求只输出 JSON,不要解释。提示词大概是这样:

复制代码
你是一个接口测试专家。下面是一个 OpenAPI operation 的 JSON。
请生成 pytest 参数化用例,输出 JSON 数组,每个元素包含:
{
  "id": "唯一ID",
  "name": "用例名",
  "path_params": {},
  "query": {},
  "headers": {},
  "body": {},
  "expected_status": [200],
  "asserts": [
    {"type": "jsonpath", "path": "$.code", "eq": 0}
  ],
  "needs_review": false
}

规则:
1. 只根据给定 schema 生成,不要发明字段;
2. 每个接口至少覆盖:正常、必填缺失、类型错误、边界值、鉴权缺失;
3. 没有 example 的字段可以用合理值,但 needs_review 标 true;
4. 写接口不要生成真实删除、支付、退款这类危险操作;
5. 不确定的断言不要硬写,标 needs_review。

调用代码:

复制代码
# tools/ai_enrich.py
import json, os, yaml
from openai import OpenAI

client = OpenAI(
    api_key=os.getenv("LLM_API_KEY"),
    base_url=os.getenv("LLM_BASE_URL"),
)

PROMPT = open("prompts/gen_cases.txt", encoding="utf-8").read()

def enrich(op):
    resp = client.chat.completions.create(
        model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
        temperature=0.2,
        messages=[
            {"role": "system", "content": "你只输出 JSON,不要 Markdown,不要解释。"},
            {"role": "user", "content": PROMPT + "\n\nOperation:\n" + json.dumps(op, ensure_ascii=False)},
        ],
    )
    return json.loads(resp.choices[0].message.content)

with open("cases/raw_ops.yaml", encoding="utf-8") as f:
    ops = yaml.safe_load(f)

all_cases = []
for op in ops:
    cases = enrich(op)
    for c in cases:
        c["operation_id"] = op["id"]
        c["method"] = op["method"]
        c["path"] = op["path"]
    all_cases.extend(cases)

with open("cases/generated.yaml", "w", encoding="utf-8") as f:
    yaml.safe_dump(all_cases, f, allow_unicode=True, sort_keys=False)

print(f"生成 {len(all_cases)} 条,待审核 {sum(1 for c in all_cases if c.get('needs_review'))} 条")

我们第一次跑出来 512 条。别急着高兴,里面有一部分是废话,比如给查询接口生成"删除成功"的断言。人工删了 43 条,补了 60 条业务规则,最终 529 条。

AI 在这里的价值不是替你拍板,是把草稿写出来。你改草稿,比从零写快得多。

用 pytest 接住这些用例

我不喜欢把用例写死成 500 个函数,太乱。用 pytest.mark.parametrize 动态加载 YAML,报告里照样一条条显示。

conftest.py

复制代码
import os
import pytest
import requests

BASE_URL = os.getenv("API_BASE_URL", "http://test-api.internal")

@pytest.fixture(scope="session")
def api():
    s = requests.Session()
    s.headers.update({
        "X-Env": "regression",
        "Authorization": f"Bearer {os.getenv('API_TOKEN')}",
    })
    yield s
    s.close()

tests/test_generated_api.py

复制代码
import json
import jsonschema
import pytest
import yaml
from jsonpath_ng import parse

def load_cases():
    with open("cases/generated.yaml", encoding="utf-8") as f:
        return yaml.safe_load(f)

def check_rule(resp, rule):
    if rule["type"] == "status":
        assert resp.status_code == rule["eq"], resp.text
    elif rule["type"] == "jsonpath":
        matches = parse(rule["path"]).find(resp.json())
        assert matches, f"jsonpath 没匹配到: {rule['path']}"
        assert matches[0].value == rule["eq"], f"{matches[0].value} != {rule['eq']}"
    else:
        raise AssertionError(f"未知断言类型: {rule['type']}")

@pytest.mark.parametrize("case", load_cases(), ids=lambda c: c["id"])
def test_generated_api(api, case):
    url = case["path"]
    for k, v in case.get("path_params", {}).items():
        url = url.replace("{" + k + "}", str(v))

    resp = api.request(
        case["method"],
        BASE_URL + url,
        params=case.get("query"),
        json=case.get("body"),
        headers=case.get("headers"),
        timeout=10,
    )

    assert resp.status_code in case["expected_status"], resp.text

    if case.get("response_schema"):
        jsonschema.validate(resp.json(), case["response_schema"])

    for rule in case.get("asserts", []):
        check_rule(resp, rule)

跑起来:

复制代码
pytest tests/test_generated_api.py -n 8 --dist loadscope -m regression

-n 8 是 pytest-xdist,8 个进程并行。写接口我单独放在 tests/test_order_write.py,不让生成器碰,因为需要造数据和清理。

断言别只断 200

这是我踩过最大的坑。第一版生成器只断 status_code == 200,结果接口返回 {"code": 500, "msg": "系统异常"} 也绿。后来加了三层:

  1. HTTP 状态码;

  2. 响应 schema 用 jsonschema 校验;

  3. 业务断言,比如 $.code == 0$.data.id 非空、金额大于 0。

AI 可以帮你生成业务断言候选,但必须人工过一遍。尤其是金额、状态机、权限这些,模型很容易想当然。

数据依赖和清理

500 条用例如果互相污染,跑一次就废。我的做法:

  • 查询类用例随便跑;

  • 写接口用例单独写,不放进生成器;

  • 必须造数据的,用 fixture,yield 后面清理;

  • 用 UUID 后缀避免重复;

  • 不依赖执行顺序,pytest 默认乱序也能跑。

简单 fixture:

复制代码
import uuid

@pytest.fixture
def new_order(api):
    order_no = f"auto_{uuid.uuid4().hex[:8]}"
    resp = api.post(BASE_URL + "/api/order/create", json={"orderNo": order_no})
    assert resp.status_code == 200
    data = resp.json()["data"]
    yield data
    api.delete(BASE_URL + f"/api/order/{data['id']}")

并行之后,3 天变 3 小时

现在 CI 里分两档:

  • MR:pytest -m smoke -n 4,大概 20 分钟;

  • 每晚:pytest -m regression -n 8,全量 529 条,3 小时左右。

为什么不是 30 分钟?因为有些接口本身慢,还有限流和测试数据准备。但对比之前 3 天手工回归,已经不是一个量级。

命令大概这样:

复制代码
pytest -n 8 \
  -m "regression and not slow" \
  --alluredir=allure-results \
  --maxfail=20

--maxfail=20 是防止一个环境挂了,后面 500 条全红,报告没法看。

几个坑,提前说

  1. AI 会编字段 。提示词里写死"只根据 schema",仍然可能编。必须人工审核 needs_review

  2. Swagger 不准,生成全废。接口文档和实际不一致,AI 只会放大这个错误。

  3. 写接口别全自动。删除、支付、退款、发货,这些让 AI 生成用例,迟早出事。

  4. 断言太弱等于没测。只断 200 的用例,不如不写。

  5. 并行不是万能。有状态接口、限流接口,该串行就串行。

  6. Token 会过期。回归跑 3 小时,中间 token 失效很常见,需要自动刷新或长有效期测试 token。

最后说点实在的

这套东西最值钱的地方,不是 AI 帮你写了多少代码,而是你把 Swagger 里沉睡的 schema 变成了可执行资产。新增接口时,跑一遍抽取和生成,人工审半小时,补十几条用例,比从零写轻松太多。

但别一上来就全量。先挑 20 个稳定接口试点,把提示词、断言规则、数据清理跑顺,再铺开。Swagger 规范是上限,AI 只是加速器。Swagger 瞎写,AI 只会帮你更快地瞎写。

相关推荐
Dawson Zhu1 小时前
大模型 Agent 记忆系统五大技术路线解析与工程选型指南
人工智能·语言模型·架构·aigc·agi
AI砖家1 小时前
AI 编程面试 20 题:Codex、Claude Code 与 AI 工具使用全攻略
人工智能·语言模型·ai编程·claude·codex
H0311169851 小时前
移动应用数据平台资料整理:月狐数据、易观千帆、艾瑞咨询
人工智能
成为深度学习高手1 小时前
EMAformer:给Transformer披上嵌入铠甲增强时间序列预测
人工智能·深度学习·数据挖掘
逐米时代1 小时前
BOM智能构建:全链路一致性自动校验
大数据·数据库·人工智能
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(76):SEEM——从碎片检索到完整事件重建
论文阅读·人工智能·学习·开源·github
zhangfeng11331 小时前
Ubuntu 版的 CANN 9.2.0-beta.1 的下载方式 三大云厂商的服务器系统
人工智能·华为·ai编程·npu·cann
唐璜Taro1 小时前
Agent Harness 系列 · 第 2篇|同一个问题三种回答
人工智能·python
YOLO数据集集合1 小时前
智慧工业工地安全防护检测数据集 | 工业安全 防护装备检测 安全帽识别 口罩检测 9097期
人工智能·目标检测·计算机视觉·目标跟踪·智慧工地·工地