主题
接口测试用例的 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 库存 1002. 完整提示词(复制即用,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.py | JSON → 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 件事:
- 把
validate_cases.py加进 CI,禁止"含编造字段"的用例入库 - 把
cases/*.json纳入评审:评审 JSON 而不是评审脚本,改动更小、更容易看
参考资料
- 本文的字段校验与"分批投喂"做法参考自开发者社区中关于 AI 辅助回归测试的分批投喂与上下文清单 的实践总结
- 分层生成(结构抽取 → 语义变异 → 用例合成)思路参考 AI 全栈测试实战(含结构化输出约束与低温采样建议)
- 断言有效性校验的思路参考 Mutation-Guided Unit Test Generation(IEEE TSE 2026)
本文由 曲奇 整理,内容由 AI 辅助整理,示例数据为脱敏演示数据;仅供学习参考,转载请注明出处。