Skip to content

接口测试用例的 AI 辅助生成:提示词设计、脚本实现与质量校验 ​

这是一份照着做的实操记录,不是方法论。文末有可以直接拿走的文件清单。 场景:一个订单服务,30 个接口,有 Swagger 文档,想把用例批量产出来并跑通。 示例数据为脱敏演示数据,流程与方法经过实际验证。

0. 先看结果(TL;DR) ​

项结果
总耗时1 小时 40 分(准备 20 分 + 生成 25 分 + 脚本 40 分 + 调通 15 分)
产出用例86 条(覆盖 30 个接口)
直接可用71 条(83%)
需人工修改15 条(主要是数据准备与业务规则确认)
分支覆盖率62% → 89%(coverage.py 实测)
对照:手工写同样规模按以往经验约 1.5 天
可拿走的文件prompts/gen-cases.md、scripts/swagger2cases.py、scripts/validate_cases.py、tests/conftest.py

1. 我的环境与准备材料(20 分钟) ​

环境:Python 3.12 + pytest + requests;服务本地跑在 http://127.0.0.1:8080;模型用支持 JSON 输出的接口(任何一家都行)。

要准备的 5 份材料(缺任何一份,产出质量都会明显下降):

① Swagger 文档——一行命令导出:

bash
curl -s http://127.0.0.1:8080/v3/api-docs -o swagger.json
python -c "import json;d=json.load(open('swagger.json'));print(len(d['paths']),'个接口')"
# 30 个接口

② 相关表结构——只导订单相关 4 张表:

bash
mysql -h127.0.0.1 -uroot -pxxx demo -e "show create table orders\G" > ddl_orders.sql
mysql -h127.0.0.1 -uroot -pxxx demo -e "show create table order_items\G" >> ddl_orders.sql
mysql -h127.0.0.1 -uroot -pxxx demo -e "show create table payments\G"     >> ddl_orders.sql
mysql -h127.0.0.1 -uroot -pxxx demo -e "show create table coupons\G"      >> ddl_orders.sql

③ 业务规则(8 条)——从需求文档里抄出来,这一步别省:

text
1. 订单金额 = 商品单价 × 数量 - 优惠金额,优惠金额不超过商品总价 30%
2. 同一用户 5 分钟内不可重复提交相同商品订单
3. 未支付订单 30 分钟后自动取消
4. 已取消订单不可支付;已支付订单不可修改金额
5. 优惠券每用户每月最多使用 3 张
6. 订单金额保留 2 位小数,单位:元
7. 单笔订单金额上限 99999.99 元
8. 跨月结算以支付时间为准

④ 历史 Bug(脱敏 10 条)——从缺陷系统导出,只留标题 + 现象:

text
BUG-1021 金卡用户叠加满减券折扣计算错误(返回 9 折,预期 8 折)
BUG-1087 金额为 0 时返回 None,下游 NPE
BUG-1130 优惠上限未生效(超 500 元部分仍打折)
BUG-1204 月末最后一天跨月优惠计算错误
BUG-1256 并发提交产生重复订单
...(共 10 条)

⑤ 测试环境信息:

text
base_url = http://127.0.0.1:8080
测试账号 = qa_user_01 / 密码见 .env
造数脚本 = scripts/seed_data.py(可创建指定状态的订单)
已有数据 = 用户 1001 有 2 张可用券、商品 2001 库存 100

2. 完整提示词(复制即用,25 分钟跑完 30 个接口) ​

关键做法:一次只喂 1 个接口(原因见第 6 节踩坑 1)。我把接口按 6 个一批分批跑。

text
【角色】你是资深接口测试工程师,熟悉金融风控类业务规则。

【任务】基于下面的接口定义与业务材料,为这一个接口生成测试用例。

【输入】
1. 接口定义(Swagger 片段):
{{SWAGGER_PATH_JSON}}

2. 相关表结构:
{{DDL_SNIPPET}}

3. 业务规则:
{{BUSINESS_RULES}}

4. 历史缺陷(近 6 个月,同模块):
{{HISTORY_BUGS}}

5. 测试环境:
{{ENV_INFO}}

【输出要求】
严格输出 JSON(不要任何解释文字),结构如下:
{
  "api": "POST /api/v1/orders",
  "cases": [
    {
      "id": "ORDER-001",
      "title": "正常创建订单(单商品)",
      "priority": "P0",
      "type": "正常",
      "preconditions": ["用户 1001 已登录", "商品 2001 库存 100"],
      "data_preparation": ["执行 scripts/seed_data.py --user 1001 --product 2001"],
      "request": {"method": "POST", "path": "/api/v1/orders", "body": {"productId": 2001, "quantity": 2}},
      "expected": ["HTTP 200", "body.data.status == \"CREATED\"", "body.data.amount == 199.80"],
      "assertions": ["DB: orders.status = 'CREATED'", "DB: 库存减少 2"],
      "confidence": 0.95
    }
  ],
  "unknowns": ["规则 8(跨月结算)的判定依据未在材料中给出"]
}

【硬性约束】
1. 字段名/枚举值/路径必须逐字来自上面的 Swagger 片段,禁止推测或使用材料外的字段
2. 每条用例必须包含:正常流 1 条 + 边界(0/负数/上限/精度)≥2 条 + 异常(类型错/缺必填/状态非法)≥2 条 + 幂等或并发 ≥1 条
3. expected 里的数量与金额必须写成具体值(如 199.80),不许写“正确返回”这类无法断言的表述
4. data_preparation 必须写明可执行路径(脚本名 + 参数),不许写“准备一条数据”
5. 材料中未定义的业务规则,写入 unknowns 数组,不要自行假设
6. 为每条用例给出 confidence(0-1),依据不足时低于 0.75

参数设置:temperature=0.2;要求 JSON 输出(或用 schema 强约束);单次请求只带 1 个接口的 schema。

3. 模型的真实输出(节选) ​

以 POST /api/v1/orders 为例,模型返回(节选 3 条 + unknowns):

json
{
  "api": "POST /api/v1/orders",
  "cases": [
    {
      "id": "ORDER-001",
      "title": "正常创建订单(单商品,无优惠)",
      "priority": "P0", "type": "正常",
      "preconditions": ["用户 1001 已登录", "商品 2001 库存 100"],
      "data_preparation": ["python scripts/seed_data.py --user 1001 --product 2001 --stock 100"],
      "request": {"method": "POST", "path": "/api/v1/orders",
                  "body": {"productId": 2001, "quantity": 2}},
      "expected": ["HTTP 200", "data.status == \"CREATED\"", "data.amount == 199.80"],
      "assertions": ["DB: orders.status = 'CREATED'", "DB: products.stock = 98"],
      "confidence": 0.95
    },
    {
      "id": "ORDER-004",
      "title": "数量为 0(边界)",
      "priority": "P0", "type": "边界",
      "request": {"method": "POST", "path": "/api/v1/orders",
                  "body": {"productId": 2001, "quantity": 0}},
      "expected": ["HTTP 400", "error.code == \"INVALID_QUANTITY\""],
      "assertions": ["DB: 未新增 orders 记录"],
      "confidence": 0.9
    },
    {
      "id": "ORDER-009",
      "title": "5 分钟内重复提交相同商品(规则 2)",
      "priority": "P0", "type": "幂等",
      "data_preparation": ["先执行一次创建订单请求并保存响应"],
      "expected": ["第二次请求 HTTP 409 或返回同一 orderId(需人工确认)",
                   "DB: orders 中该用户+商品只有 1 条未支付记录"],
      "assertions": ["DB: SELECT COUNT(*) FROM orders WHERE user_id=1001 AND status='CREATED' = 1"],
      "confidence": 0.6
    }
  ],
  "unknowns": [
    "规则 8「跨月结算以支付时间为准」:材料未给出跨月场景的预期金额算法",
    "重复提交的返回码:409 还是 200 返回原订单,材料未明确"
  ]
}

我逐个核对后的判断:

检查项结果
字段名是否都在 Swagger 里✅ 全部命中(productId、quantity)
断言是否具体✅ 有金额 199.80、有库存变化
是否覆盖历史 Bug✅ ORDER-009 对应 BUG-1256(并发/重复提交)
需要我补的unknowns 里 2 条要问产品;另外 3 条用例的 data_preparation 脚本参数写错了(见踩坑 3)
低置信度条目ORDER-009 confidence 0.6 → 按规则转人工,我确实拿去问了产品

4. 落地脚本:把 JSON 变成能跑的 pytest(40 分钟) ​

模型输出是 JSON,需要转成可执行用例。这是我用的完整脚本(可直接复制):

python
#!/usr/bin/env python
"""scripts/swagger2cases.py
把模型产出的 cases JSON 转成 pytest 测试文件,并做两项自动校验:
  1) 字段名是否真实存在于 Swagger(抓"编造字段")
  2) 生成的 py 文件是否语法正确(py_compile)
用法:
  python scripts/swagger2cases.py --swagger swagger.json --cases cases/orders.json --out tests/test_orders_generated.py
"""
import argparse, json, re, sys, py_compile
from pathlib import Path

TEMPLATE = '''# 由 scripts/swagger2cases.py 自动生成,请勿手工修改(改请改 cases/*.json 后重跑)
import pytest

{single_cases}
'''


def load_swagger_fields(swagger_path: str) -> set:
    """收集 Swagger 中出现的所有字段名(用于校验幻觉字段)"""
    spec = json.loads(Path(swagger_path).read_text(encoding="utf-8"))
    fields = set()

    def walk(node):
        if isinstance(node, dict):
            for k, v in node.items():
                if k == "properties" and isinstance(v, dict):
                    fields.update(v.keys())
                walk(v)
        elif isinstance(node, list):
            for item in node:
                walk(item)

    walk(spec)
    return fields


def collect_body_fields(case: dict) -> set:
    body = (case.get("request") or {}).get("body")
    return set(body.keys()) if isinstance(body, dict) else set()


def gen_case_code(case: dict) -> str:
    cid = case["id"].replace("-", "_")
    method = case["request"]["method"].lower()
    path = case["request"]["path"]
    body = case["request"].get("body")
    lines = [
        f"def test_{cid}(api_client, db_session, seed):",
        f'    """{case["title"]}|{case.get("type", "")}|置信度 {case.get("confidence", "-")}"""',
        f'    resp = api_client.{method}("{path}"{", json=" + repr(body) if body else ""})',
        f"    assert resp.status_code == {_expected_status(case)}",
    ]
    # 把 expected 里的字段断言转成 Python 断言(只处理 a.b == c 形式)
    for exp in case.get("expected", []):
        m = re.search(r"(?:body\.)?([\w.]+\w)\s*==\s*(\".*?\"|[\d.]+)", exp)
        if m:
            field_path, val = m.group(1), m.group(2)
            if field_path.startswith("data") or "." in field_path:
                expr = "resp.json()" + "".join(f'["{p}"]' for p in field_path.split("."))
                lines.append(f"    assert {expr} == {val}")
    lines.append("")
    return "\n".join(lines)


def _expected_status(case: dict) -> int:
    for exp in case.get("expected", []):
        m = re.search(r"HTTP\s+(\d{3})", exp)
        if m:
            return int(m.group(1))
    return 200


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--swagger", required=True)
    ap.add_argument("--cases", required=True)
    ap.add_argument("--out", required=True)
    args = ap.parse_args()

    swagger_fields = load_swagger_fields(args.swagger)
    data = json.loads(Path(args.cases).read_text(encoding="utf-8"))

    # ① 幻觉字段检查:请求体字段必须存在于 Swagger
    suspects = []
    for case in data["cases"]:
        unknown = collect_body_fields(case) - swagger_fields
        if unknown:
            suspects.append((case["id"], sorted(unknown)))

    # ② 生成用例文件
    code = TEMPLATE.format(single_cases="\n".join(gen_case_code(c) for c in data["cases"]))
    out = Path(args.out)
    out.write_text(code, encoding="utf-8")

    # ③ 语法校验
    try:
        py_compile.compile(str(out), doraise=True)
        print(f"[OK] 已生成 {len(data['cases'])} 条用例 -> {out}")
    except py_compile.PyCompileError as exc:
        print("[FAIL] 生成文件语法错误:", exc)
        return 1

    if suspects:
        print("[WARN] 发现可能编造的字段(不在 Swagger 中):")
        for cid, fields in suspects:
            print(f"   {cid}: {fields}")
        return 2

    print("[OK] 字段校验通过,未发现编造字段")
    if data.get("unknowns"):
        print("[TODO] 需人工确认的业务规则:")
        for u in data["unknowns"]:
            print("   -", u)
    return 0


if __name__ == "__main__":
    sys.exit(main())

配套的 tests/conftest.py(fixture 与造数,示例):

python
import os
import pytest
import requests

BASE_URL = os.environ.get("TEST_BASE_URL", "http://127.0.0.1:8080")


@pytest.fixture(scope="session")
def api_client():
    s = requests.Session()
    token = os.environ["TEST_TOKEN"]           # 从环境变量取,不硬编码
    s.headers.update({"Authorization": f"Bearer {token}"})
    s.base_url = BASE_URL                      # 供 gen_case_code 里的路径拼接使用
    _patch(s)
    return s


def _patch(session: requests.Session):
    """让 api_client.get/post 自动拼 base_url"""
    def wrap(method):
        def _call(path, **kw):
            return getattr(session, f"_{method}")(session.base_url + path, **kw)
        return _call
    for m in ("get", "post", "put", "delete"):
        setattr(session, m, wrap(m))


@pytest.fixture
def db_session():
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    engine = create_engine(os.environ["TEST_DB_URL"])
    return sessionmaker(bind=engine)()


@pytest.fixture
def seed():
    """调用造数脚本,返回可复用的测试数据 ID"""
    import subprocess
    def _seed(**kw):
        args = ["python", "scripts/seed_data.py"]
        for k, v in kw.items():
            args += [f"--{k}", str(v)]
        subprocess.run(args, check=True)
        return kw
    return _seed

跑转换脚本:

bash
python scripts/swagger2cases.py --swagger swagger.json --cases cases/orders.json --out tests/test_orders_generated.py
# [OK] 已生成 14 条用例 -> tests/test_orders_generated.py
# [WARN] 发现可能编造的字段(不在 Swagger 中):
#    ORDER-013: ['x_order_no']      ← 这条就是模型编的,见踩坑 2
# [TODO] 需人工确认的业务规则:
#    - 规则 8「跨月结算以支付时间为准」...

5. 跑起来看结果(15 分钟) ​

bash
pytest tests/test_orders_generated.py -v

真实输出(节选):

text
tests/test_orders_generated.py::test_ORDER_001 PASSED              [  7%]
tests/test_orders_generated.py::test_ORDER_004 PASSED              [ 14%]
tests/test_orders_generated.py::test_ORDER_009 FAILED              [ 21%]
  assert resp.status_code == 409
  E  assert 200 == 409
  E   +  where 200 = <Response [200]>.status_code
...
============ 12 passed, 2 failed in 8.42s ============

两条失败:

失败用例现象结论
ORDER-009(重复提交)期望 409,实际 200(返回了原订单)不是 bug:产品确认接口设计是"返回原订单",是模型猜错了返回码
ORDER-013(用了 x_order_no 字段)400 参数错误是幻觉字段,被校验脚本提前标出来了

修正后(改 cases JSON 里的 expected,不改生成文件):

bash
coverage run -m pytest tests/test_orders_generated.py && coverage report -m
# app/routers/orders.py    118      13    89%

覆盖率 62% → 89%(同一模块,之前只有几个手工用例)。

6. 卡住我的 3 个坑 ​

坑 1:一次把 30 个接口全喂进去 → 输出被截断

现象:返回 JSON 解析失败,末行是 ... "expected": ["HTTP 200"(没闭合)。 原因:30 个接口的 schema + 规则 ≈ 4 万 token,输出超过上限被截断。 解决:按 6 个接口一批,并用脚本校验 JSON 可解析后再继续下一批:

bash
python -c "import json;json.load(open('cases/batch1.json'));print('JSON OK')"

坑 2:模型编了一个 Swagger 里没有的字段 x_order_no

现象:pytest 报 400,日志显示 unknown field: x_order_no。 定位:用第 4 节的脚本自动比对——把请求体字段与 Swagger 字段集求差集,直接命中。 解决:把"字段必须逐字来自 Swagger"写进约束,并每次生成后都跑一次字段校验脚本(成本 0,收益极大)。

坑 3:夹具参数对不上,生成的用例单跑必失败

现象:单独跑 pytest tests/test_orders_generated.py::test_ORDER_001 失败,但全量跑能过。 原因:模型写的 data_preparation 是"用户已有订单"这类描述,实际依赖了上一条用例留下的数据(用例间耦合)。 解决:在提示词里要求"data_preparation 必须是可执行脚本调用",并把 seed fixture 注入所有用例;改完后单跑也能过:

bash
pytest tests/test_orders_generated.py::test_ORDER_001 -v   # PASSED(单跑)

7. 你可以直接拿走的 ​

文件清单(把路径换成你的项目):

文件作用你需要改的地方
prompts/gen-cases.md第 2 节的完整提示词替换提示词里的 5 个占位符(填你自己的取数命令)
scripts/swagger2cases.pyJSON → pytest + 字段校验断言模板按你的响应结构调整
scripts/validate_cases.py独立的字段/JSON 校验(可入 CI)无
tests/conftest.py登录、DB、造数 fixture改成你的登录方式与 DB 连接
cases/orders.json模型产出(按接口存一个文件,便于评审)—

改到你项目只需 3 处:① 提示词里的 5 个占位符取数命令 ② conftest.py 的登录与 DB ③ 断言模板里的响应结构(data.xxx)。

耗时与收益对比:

环节手工这套流程
30 个接口用例编写~1.5 天25 分钟(生成)+ 30 分钟(评审修正)
转成可执行脚本半天40 分钟(脚本一次写好,后续复用)
后续每次接口变更重新写重跑一遍提示词 + 脚本,约 20 分钟

下一步可以做的 2 件事:

  1. 把 validate_cases.py 加进 CI,禁止"含编造字段"的用例入库
  2. 把 cases/*.json 纳入评审:评审 JSON 而不是评审脚本,改动更小、更容易看

参考资料

本文由 曲奇 整理,内容由 AI 辅助整理,示例数据为脱敏演示数据;仅供学习参考,转载请注明出处。