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

举报
霍格沃兹测试开发 发表于 2026/09/20 16:21:52 2026/09/20
【摘要】 上个月我把订单中台的回归测试从 Postman 里拽了出来。之前 87 个接口,每次发版前手工点,3 天起步,点到后面人都麻了。现在 CI 上跑 500 多条 pytest,全量回归 3 小时出头,MR 只跑 smoke 的话 20 分钟。核心不是哪个神器,而是把 Swagger 当成数据源,让 AI 补测试场景,再让 pytest 去干重复劳动。下面是我实际落地的过程,代码是删减过的,但骨...

上个月我把订单中台的回归测试从 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 只会帮你更快地瞎写。

关于我们

本文部分内容参考了霍格沃兹测试开发学社整理的相关技术资料,主要涉及软件测试、自动化测试、测试开发及 AI 测试等内容,侧重测试实践、工具应用与工程经验整理。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。