主题
AI 辅助生成接口自动化脚本的实践与局限(50 例实测)
前两篇讲完了"生成用例"和"校验用例"。这篇是下一步:把用例变成能跑的 pytest 脚本。 我拿 50 条用例做了实测:42 条一次跑通(84%),8 条要改。下面把提示词、脚手架、这 8 条的具体问题全部贴出来。 文中脚本与数据为脱敏示例,流程经过实测。
0. 先看结果(TL;DR)
| 项 | 结果 |
|---|---|
| 生成对象 | 50 条接口用例 → 50 个 pytest 测试函数 |
| 一次跑通 | 42 条(84%) |
| 需修改 | 8 条(硬编码 3、fixture 引用错 2、断言弱 1、DB 表名错 1、异常构造假 1) |
| 耗时 | 生成 12 分钟 + 框架准备 25 分钟 + 修正 8 条 18 分钟 ≈ 55 分钟 |
| 手工写同等规模 | 按以往经验约 1.5 天 |
| 全部通过率 | 修正后 50/50 通过,分支覆盖率 71% → 93% |
| 可拿走 | prompts/gen-scripts.md、tests/conftest.py、scripts/gen_scripts.py、.env.example |
1. 环境与材料(准备 25 分钟,一次性投入)
框架先立好(这步不能省,否则 AI 会各写各的):
text
tests/
├── conftest.py # fixture:登录、基础 URL、DB 会话、造数
├── .env.example # 环境变量样例(AI 必须照着用)
├── test_orders_generated.py
└── fixtures/seed_data.py.env.example(给 AI 看的"环境变量白名单"):
bash
TEST_BASE_URL=http://127.0.0.1:8080
TEST_TOKEN= # 从登录接口获取,不要硬编码
TEST_DB_URL=sqlite:///./data/orders.dbconftest.py 里的 fixture 名(AI 只能用这些):
text
api_client → 已带 Authorization 头与 base_url 的 requests.Session
db_session → SQLAlchemy Session(可执行 SQL)
seed(**kw) → 调 scripts/seed_data.py 造数,返回可复用数据
ORDER_DB_TABLES → 表名字典,如 {"orders": "orders", "items": "order_items"}2. 完整提示词(复制即用)
text
【角色】你是 Python 自动化测试开发工程师。
【任务】把下面这批接口用例,转成可执行的 pytest + requests 测试脚本。
【输入】
1. 用例(JSON):{{CASES_JSON}}
2. 现有框架 fixture 清单(只能用这些,不得自行创建新 fixture):
{{FIXTURE_LIST}}
3. 环境变量白名单(只能使用这些,不得硬编码地址/账号):
{{ENV_VARS}}
4. 表名字典:{{TABLE_DICT}}
5. 一个已有用例文件作为风格示例:
{{STYLE_EXAMPLE}}
【输出格式】
- 只输出一个完整的 .py 文件内容,不要解释文字,不要 markdown 代码围栏
- 每个用例一个测试函数,函数名 = test_ + 用例 id(下划线连接)
- 文件顶部只 import 必要模块(pytest / sqlalchemy.text 等)
【硬性约束】
1. 禁止出现任何硬编码的 URL、账号、token、密码;一律从 fixture 或环境变量取
2. 只能使用上面列出的 fixture 名;不确定就用 seed() 造数,不要引用不存在的 fixture
3. 断言必须具体:校验业务字段值(如响应 JSON 的金额、状态)+ 至少一项副作用(DB 查询结果)
4. 每条用例必须能独立运行(不依赖其它用例产生的数据)
5. 需要 DB 校验时,SQL 里的表名必须来自表名字典
6. 异常场景(超时、500、依赖失败)用 monkeypatch 或参数化构造,禁止用 time.sleep 模拟
7. 若某条用例的信息不足以生成脚本,用 pytest.skip("信息不足:<原因>") 标记,不要编造参数:temperature=0.2;一次喂 10-15 条用例(太多会输出截断,见踩坑 3)。
3. 模型真实输出(节选)
python
import pytest
from sqlalchemy import text
def test_ORDER_001_create_order_normal(api_client, db_session, seed):
"""正常创建订单(单商品,无优惠)"""
seed(user=1001, product=2001, stock=100)
resp = api_client.post("/api/v1/orders",
json={"productId": 2001, "quantity": 2})
assert resp.status_code == 200
body = resp.json()["data"]
assert body["status"] == "CREATED"
assert body["amount"] == "199.80"
stock = db_session.execute(
text("SELECT stock FROM products WHERE id = :pid"), {"pid": 2001}
).scalar()
assert stock == 98
def test_ORDER_004_quantity_zero(api_client, db_session):
"""数量为 0(边界)"""
resp = api_client.post("/api/v1/orders",
json={"productId": 2001, "quantity": 0})
assert resp.status_code == 400
assert resp.json()["error"]["code"] == "INVALID_QUANTITY"
cnt = db_session.execute(text("SELECT COUNT(*) FROM orders")).scalar()
assert cnt == 0这份是 42 条"直接可用"里的典型:fixture 用对了、断言有具体值、有 DB 副作用校验。可用的那部分特征非常一致——这正是因为提示词把它约束住了。
4. 那 8 条卡在哪(逐条对照)
| # | 用例 | 现象 | 根因 | 修法 |
|---|---|---|---|---|
| 1 | ORDER-012 | requests.exceptions.MissingSchema: Invalid URL '/api/v1/orders': No scheme supplied | 直接用了 requests.post,没用 api_client | 改成 api_client.post(...) |
| 2 | ORDER-018 | 同上 | 同上 | 同上 |
| 3 | ORDER-026 | URL 写成 http://localhost:8080/... | 硬编码地址 | 交给 api_client(其 base_url 来自 TEST_BASE_URL) |
| 4 | ORDER-031 | 硬编码 Authorization: Bearer test-token | 硬编码 token | 删除,由 api_client 注入 |
| 5 | ORDER-037 | fixture 'auth_headers' not found | 引用了不存在的 fixture(自创) | 改用 api_client |
| 6 | ORDER-041 | fixture 'order_factory' not found | 同上 | 改用 seed(order_status="CREATED") |
| 7 | ORDER-045 | assert resp.status_code == 200(唯一断言) | 断言弱 | 补 body["data"]["couponCount"] == 2 + DB 校验 |
| 8 | ORDER-049 | Table 'demo.order_list' doesn't exist | 表名写错(字典里是 orders) | 按表名字典改为 orders |
| 9 | ORDER-050 | time.sleep(5) 后断言超时 | 异常场景用 sleep 假造 | 改 monkeypatch 让下游抛 Timeout |
8 条里有 6 条属于同一类根因:没遵守"只能用给定 fixture / 不准硬编码"。所以修正方式不是逐条改,而是:
- 把
fixture清单与.env.example放进提示词输入(已在第 2 节) - 修正完这 8 条后,把它们的错误写法与正确写法做成 few-shot 反例,塞回提示词 → 下一批 50 条里,同类问题从 6 条降到 0 条(我实际又跑了 30 条验证)
5. 配套脚手架(可直接复制)
scripts/gen_scripts.py(调用模型 + 生成文件 + 静态检查):
python
#!/usr/bin/env python
"""scripts/gen_scripts.py
把用例 JSON 批量化生成 pytest 脚本,并做两项自动校验:
1) 是否出现硬编码(URL / token / 密码)
2) 生成文件语法是否正确(py_compile)
用法:
python scripts/gen_scripts.py --cases cases/orders.json --out tests/test_orders_generated.py --batch 12
"""
import argparse, json, py_compile, re, sys
from pathlib import Path
HARDCODE_PATTERNS = [
(re.compile(r"https?://(?!127\.0\.0\.1)"), "疑似硬编码外部 URL"),
(re.compile(r"(Bearer|token|password)\s*[:=]\s*[\"'][^\"']+[\"']", re.I), "疑似硬编码凭证"),
(re.compile(r"time\.sleep\("), "禁止用 time.sleep 模拟异常"),
]
def call_model(prompt: str) -> str:
"""替换成你的模型调用(保持返回纯 .py 文本)"""
raise NotImplementedError("在此接入你的模型调用")
def lint(code: str) -> list:
issues = []
for pattern, msg in HARDCODE_PATTERNS:
if pattern.search(code):
issues.append(msg)
return issues
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cases", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--batch", type=int, default=12, help="每批用例条数(过多会截断)")
args = ap.parse_args()
cases = json.loads(Path(args.cases).read_text(encoding="utf-8"))["cases"]
header = (
"# 由 scripts/gen_scripts.py 自动生成;修改请改用例 JSON 后重跑\n"
"import pytest\nfrom sqlalchemy import text\n\n\n"
)
body, all_issues = [], []
for i in range(0, len(cases), args.batch):
chunk = cases[i:i + args.batch]
prompt = build_prompt(chunk) # 见第 2 节的提示词模板
code = call_model(prompt)
all_issues += lint(code)
body.append(code.rstrip())
out = Path(args.out)
out.write_text(header + "\n\n".join(body) + "\n", encoding="utf-8")
try:
py_compile.compile(str(out), doraise=True)
print(f"[OK] 生成 {len(cases)} 条用例 -> {out}")
except py_compile.PyCompileError as exc:
print("[FAIL] 语法错误:", exc)
return 1
if all_issues:
print("[WARN] 静态检查命中:")
for it in sorted(set(all_issues)):
print(" -", it)
return 2
print("[OK] 静态检查通过(无硬编码、无 sleep)")
return 0
if __name__ == "__main__":
sys.exit(main())运行与结果:
bash
python scripts/gen_scripts.py --cases cases/orders.json --out tests/test_orders_generated.py --batch 12
# [WARN] 静态检查命中:
# - 疑似硬编码外部 URL
# - 疑似硬编码凭证
# - 禁止用 time.sleep 模拟异常
pytest tests/test_orders_generated.py -q
# 42 passed, 8 failed in 24.31s修完 8 条后:
bash
pytest tests/test_orders_generated.py -q
# 50 passed in 31.02s
coverage run -m pytest tests/test_orders_generated.py && coverage report -m
# app/routers/orders.py 131 9 93%6. 3 个卡住我的坑
坑 1:模型自创 fixture 名,跑起来直接 ERROR
现象:fixture 'auth_headers' not found;我翻遍 conftest.py 根本没这个 fixture。 原因:我在提示词里只写了"复用现有 fixture",但没把 fixture 清单列给它,模型只能按通用经验猜(auth_headers 是它见过的常见命名)。 解决:把 fixture 名 + 用途列成清单放进输入,并加一句"不得创建或引用清单外的 fixture"。同类错误从 6 条降到 0 条。 顺带一提:pytest --fixtures 可以直接导出你项目的 fixture 清单,作为提示词输入很方便:
bash
pytest --fixtures -q | head -40 > prompts/fixture_list.txt坑 2:生成的脚本一次性全跑没问题,单独跑就挂
现象:pytest tests/test_orders_generated.py 通过,但 pytest ... ::test_ORDER_031 失败。 原因:脚本用了前一条用例遗留的数据(模型按顺序"思考")。 解决:在提示词里把"必须能独立运行"写成显式约束,并在 CI 里加一条乱序执行校验:
bash
pytest tests/test_orders_generated.py -p no:randomly --random-order # 或
pytest tests/test_orders_generated.py -k "ORDER_031" # 单条跑坑 3:一次喂 50 条用例 → 输出被截断,文件写到一半
现象:生成文件末尾是 def test_ORDER_038(api_client 就断了,py_compile 报语法错误。 原因:50 条用例 + fixture 清单 + 风格示例 ≈ 3.8 万 token 输出,超出上限。 解决:按 12 条一批(--batch 12),每批生成后立即 py_compile 校验,失败就重试该批。
7. 你可以直接拿走的
| 文件 | 作用 | 要改的地方 |
|---|---|---|
prompts/gen-scripts.md | 第 2 节完整提示词 | 替换 5 个占位符(fixture 清单、env 白名单、表名字典) |
tests/conftest.py | api_client / db_session / seed fixtures | 改成你的登录与 DB 连接 |
scripts/gen_scripts.py | 批量生成 + 硬编码扫描 + 语法校验 | 接入你的模型调用 |
.env.example | 环境变量白名单 | 按你的项目补变量 |
收益对比:
| 指标 | 手工 | 本流程 |
|---|---|---|
| 50 条脚本编写 | 约 1.5 天 | 12 分钟生成 + 18 分钟修正 |
| 硬编码检查 | 靠 code review | 静态扫描自动拦(退出码 2 告警) |
| 第二次生成同类错误 | — | 从 6 条降到 0 条(把反例写进提示词后) |
下一步可以做的 2 件事:
- 把"修正过的 8 条"做成反例 few-shot 放回提示词 —— 这是投入产出比最高的一步
- 把
gen_scripts.py接进 CI:生成脚本 → 静态扫描 → 语法校验 → 自动开 PR 等评审(不要直接进主干)
参考资料
- 脚本生成可用率与剩余人工修改部分的实践参考:AI 辅助回归测试实战
- 生成物必须可提交、门禁内不调用模型:Agentic QA Tools
- 断言强度与变异分数:Mutation-Guided Unit Test Generation(IEEE TSE 2026)
本文由 曲奇 整理,内容由 AI 辅助整理,示例脚本与数据为脱敏演示数据;仅供学习参考,转载请注明出处。