主题
AI 生成测试用例的质量校验:六类高频缺陷与自动化检查方案
上一篇《我用 AI 给 30 个接口生成用例》跑完后,手上是 86 条用例 JSON。 但"生成出来"不等于"能用"。这篇记录我怎么把 86 条逐条过筛:抓到 6 类问题共 23 处,写了校验脚本后需要人工处理的只剩 4 处。 文中数据为脱敏示例数据,脚本与方法经过实测。
0. 先看结果(TL;DR)
| 项 | 结果 |
|---|---|
| 检查对象 | 30 个接口产出的 86 条用例 |
| 发现问题 | 6 类 / 23 处 |
| 问题分布 | 编造字段 5、猜业务规则 4、断言空洞 6、数据准备不可执行 4、用例耦合 3、重复冗余 1 |
| 人工处理量 | 写脚本前需逐条看 86 条;有脚本后只剩 4 处要判断 |
| 本次耗时 | 40 分钟(写脚本 25 分 + 跑与修正 15 分) |
| 脚本收益 | 这 6 类问题每次都自动抓,后续每批生成只需 2 分钟 |
| 可拿走的文件 | scripts/validate_cases.py、scripts/case_lint_rules.md、CI 接入片段 |
1. 材料与环境
- 输入:
cases/*.json(上一流程产出,按接口一个文件) - 环境:Python 3.12;有
swagger.json(字段白名单来源) - 为什么必须有 swagger:判断"字段是不是编的"只能靠它比对,人工肉眼看不出来
2. 抓到的 6 类问题(每类都有真实样本)
① 编造字段(5 处)
AI 原始输出:
json
{
"id": "ORDER-013",
"request": {"method": "POST", "path": "/api/v1/orders",
"body": {"x_order_no": "SN20260921001", "productId": 2001, "quantity": 1}}
}问题:x_order_no 在 Swagger 里根本不存在 —— 模型按"订单号"这个常见概念自己补了一个。 修正:删掉该字段(订单号由服务端生成),改为在 assertions 里断言返回体包含 orderNo。
② 猜业务规则(4 处)
AI 原始输出:
json
{"expected": ["第二次提交返回 HTTP 409"],
"confidence": 0.6}问题:规则 2 只说"5 分钟内不可重复提交",没说返回码是 409 —— 模型按"冲突用 409"的通用经验猜的。实测接口返回 200 + 原订单。 修正:把"接口设计返回原订单"写回业务规则文档,并更新该用例期望值;同时把这条记进 unknowns 供产品确认。
③ 断言空洞(6 处)
AI 原始输出:
json
{"expected": ["HTTP 200", "返回成功"],
"assertions": []}问题:返回成功 无法断言;assertions 为空 → 这条用例永远通过,抓不到任何缺陷。 修正:补具体值 —— body.data.amount == 199.80、DB: products.stock = 98。
④ 数据准备不可执行(4 处)
AI 原始输出:
json
{"data_preparation": ["用户已有一个未支付订单", "优惠券可用"]}问题:这是"状态描述"而非"执行动作",跑起来第一步就卡住。 修正:改成可执行调用 —— python scripts/seed_data.py --user 1001 --order-status CREATED --coupon 2。
⑤ 用例耦合(3 处)
AI 原始输出:
json
{"id": "ORDER-021", "preconditions": ["已执行 ORDER-020 并保存 orderId"]}问题:单独跑这条必然失败(ORDER-020 没跑);CI 里并行执行也会失败。 修正:改为自带造数(seed(order_status="CREATED")),把 orderId 从造数结果取。
⑥ 重复冗余(1 处)
AI 原始输出:5 条用例只改了 quantity(2→3→4→5→6),标题都是"正常创建订单"。 问题:有效覆盖没增加,维护成本 ×5。 修正:合并为 1 条参数化用例 quantity=[2,3,4,5,6],另补 1 条真正的边界(quantity=1 与 quantity=999)。
问题分布汇总:
| 类型 | 数量 | 可由脚本自动判定 | 必须人工判断 |
|---|---|---|---|
| 编造字段 | 5 | ✅(对比 Swagger) | — |
| 猜业务规则 | 4 | ⚠️ 部分(confidence < 0.75 标记) | ✅(需产品确认) |
| 断言空洞 | 6 | ✅(规则匹配) | — |
| 数据准备不可执行 | 4 | ✅(无脚本/接口调用即判不合格) | — |
| 用例耦合 | 3 | ✅(引用了其他用例 ID) | — |
| 重复冗余 | 1 | ✅(相似度阈值) | — |
3. 完整校验脚本(可直接复制)
python
#!/usr/bin/env python
"""scripts/validate_cases.py
对 AI 生成的用例 JSON 做 6 项静态校验(对应 6 类高频问题)。
用法:
python scripts/validate_cases.py --swagger swagger.json --cases cases/orders.json [--strict]
退出码:
0 = 全部通过 1 = 有必须修的问题 2 = 有需人工确认项
"""
import argparse, json, re, sys
from difflib import SequenceMatcher
from pathlib import Path
# ---------- 1) 编造字段:请求体字段必须在 Swagger 中出现 ----------
def swagger_fields(spec: dict) -> set:
found = set()
def walk(node):
if isinstance(node, dict):
for k, v in node.items():
if k == "properties" and isinstance(v, dict):
found.update(v.keys())
walk(v)
elif isinstance(node, list):
for i in node:
walk(i)
walk(spec)
return found
def check_fake_fields(cases, allowed):
bad = []
for c in cases:
body = (c.get("request") or {}).get("body") or {}
if isinstance(body, dict):
unknown = set(body) - allowed
if unknown:
bad.append((c["id"], sorted(unknown)))
return bad
# ---------- 2) 断言空洞:必须含具体值断言 ----------
WEAK = re.compile(r"^(HTTP\s+\d{3}|返回成功|成功|OK|请求成功)$")
CONCRETE = re.compile(r"==\s*(\".*?\"|[\d.]+|true|false)", re.I)
def check_weak_assertions(cases):
bad = []
for c in cases:
exps = c.get("expected") or []
asserts = c.get("assertions") or []
if not asserts:
bad.append((c["id"], "assertions 为空"))
continue
if all(WEAK.match(e.strip()) for e in exps) and not any(CONCRETE.search(e) for e in exps):
bad.append((c["id"], "只断言状态码/文字,无具体值"))
return bad
# ---------- 3) 数据准备不可执行 ----------
EXECUTABLE = re.compile(r"(python\s+\S+\.py|curl\s|POST\s+/|接口|脚本)", re.I)
STATE_ONLY = re.compile(r"(已|存在).{0,6}(订单|用户|券|数据)")
def check_data_prep(cases):
bad = []
for c in cases:
preps = c.get("data_preparation") or []
if not preps:
bad.append((c["id"], "缺少 data_preparation"))
continue
if not any(EXECUTABLE.search(p) for p in preps):
bad.append((c["id"], f"不可执行的状态描述:{preps[0][:30]}"))
return bad
# ---------- 4) 用例耦合:引用了其它用例 ----------
COUPLE = re.compile(r"(已执行|执行完|上一条|前一条|依赖\s*(ORDER|CASE)-\d+)", re.I)
def check_coupling(cases):
bad = []
for c in cases:
text = " ".join(c.get("preconditions", [])) + " " + " ".join(c.get("data_preparation", []))
if COUPLE.search(text):
bad.append((c["id"], text.strip()[:60]))
return bad
# ---------- 5) 重复冗余:标题+请求相似度 > 0.9 ----------
def check_duplicates(cases, threshold=0.9):
bad = []
for i, a in enumerate(cases):
for b in cases[i + 1:]:
sig_a = f"{a.get('title','')}{json.dumps(a.get('request',{}), sort_keys=True, ensure_ascii=False)}"
sig_b = f"{b.get('title','')}{json.dumps(b.get('request',{}), sort_keys=True, ensure_ascii=False)}"
ratio = SequenceMatcher(None, sig_a, sig_b).ratio()
if ratio > threshold:
bad.append((a["id"], b["id"], round(ratio, 3)))
return bad
# ---------- 6) 低置信度:需转人工 ----------
def check_low_confidence(cases, floor=0.75):
return [(c["id"], c.get("confidence")) for c in cases
if isinstance(c.get("confidence"), (int, float)) and c["confidence"] < floor]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--swagger", required=True)
ap.add_argument("--cases", required=True)
args = ap.parse_args()
spec = json.loads(Path(args.swagger).read_text(encoding="utf-8"))
data = json.loads(Path(args.cases).read_text(encoding="utf-8"))
cases = data.get("cases", [])
report = {
"编造字段": check_fake_fields(cases, swagger_fields(spec)),
"断言空洞": check_weak_assertions(cases),
"数据准备不可执行": check_data_prep(cases),
"用例耦合": check_coupling(cases),
"重复冗余": check_duplicates(cases),
"需人工确认(低置信度)": check_low_confidence(cases),
}
must_fix = sum(len(v) for k, v in report.items() if k != "需人工确认(低置信度)")
for name, items in report.items():
flag = "TODO" if name.startswith("需人工") else "FAIL"
if items:
print(f"[{flag}] {name}({len(items)} 处)")
for it in items[:10]:
print(" ", it)
if data.get("unknowns"):
print(f"[TODO] 待业务确认规则({len(data['unknowns'])} 条)")
for u in data["unknowns"][:5]:
print(" ", u)
print(f"\n汇总:必须修 {must_fix} 处,需人工判断 {len(report['需人工确认(低置信度)'])} 处")
return 1 if must_fix else (2 if report["需人工确认(低置信度)"] else 0)
if __name__ == "__main__":
sys.exit(main())真实运行输出(修正前):
text
$ python scripts/validate_cases.py --swagger swagger.json --cases cases/orders.json
[FAIL] 编造字段(5 处)
('ORDER-013', ['x_order_no'])
('ORDER-027', ['payChannel'])
('ORDER-031', ['discountRate'])
('ORDER-044', ['refundFlag'])
('ORDER-058', ['invoiceType'])
[FAIL] 断言空洞(6 处)
('ORDER-003', '只断言状态码/文字,无具体值')
...
[FAIL] 数据准备不可执行(4 处)
('ORDER-017', '不可执行的状态描述:用户已有一个未支付订单')
...
[FAIL] 用例耦合(3 处)
('ORDER-021', '已执行 ORDER-020 并保存 orderId')
...
[FAIL] 重复冗余(1 处)
('ORDER-005', 'ORDER-006', 0.947)
[TODO] 需人工确认(低置信度)(4 处)
('ORDER-009', 0.6)
...
汇总:必须修 19 处,需人工判断 4 处修正(改 cases/*.json,不改生成器)后复跑:
text
$ python scripts/validate_cases.py --swagger swagger.json --cases cases/orders.json
[TODO] 需人工确认(低置信度)(4 处)
汇总:必须修 0 处,需人工判断 4 处19 处机械问题全部由脚本抓完,我只花了 15 分钟处理那 4 处需要业务判断的(拿去问产品)。
4. 3 个卡住我的坑
坑 1:字段校验漏了嵌套对象,误判为"通过"
现象:payment: {"channel": "ALIPAY"} 里的 channel 拼错成 payChannel,脚本却报通过。 原因:我第一次写的对比只取了 body 的顶层键(payment 在白名单里)→ 嵌套字段没检查。 解决:递归展开请求体,逐层比对:
python
def flatten_keys(obj, prefix=""):
out = set()
if isinstance(obj, dict):
for k, v in obj.items():
out.add(k); out |= flatten_keys(v, k)
elif isinstance(obj, list):
for i in obj:
out |= flatten_keys(i)
return out坑 2:相似度阈值 0.9 太宽,把"真边界用例"也标成重复
现象:quantity=999 与 quantity=1000 被判重复(比值 0.94),但它们分别是"上限内"与"超限",是两条必要用例。 原因:请求体只差一个数字,字符串相似度必然高。 解决:重复判定加业务维度——如果两条用例的 type 不同(正常/边界/异常)或期望值不同,则不判重复:
python
if a.get("type") != b.get("type") or a.get("expected") != b.get("expected"):
continue # 期望不同 → 不是重复坑 3:正则把"接口调用"当成不可执行描述
现象:data_preparation: ["调用 POST /api/v1/coupons 创建优惠券"] 被判为"不可执行的状态描述"。 原因:规则里的可执行关键词只写了 python xxx.py,漏了"调用 + 接口"这种写法。 解决:把判定改为白名单 + 兜底——既能识别脚本调用,也能识别显式接口调用(见脚本里的 EXECUTABLE 正则),并允许在 case_lint_rules.md 里追加团队习惯写法。
5. 你可以直接拿走的
| 文件 | 作用 | 你要改的地方 |
|---|---|---|
scripts/validate_cases.py | 6 项静态校验,返回退出码可直接进 CI | 相似度阈值、数据准备的团队习惯写法 |
scripts/case_lint_rules.md | 6 类问题的判定标准(含反例) | 补充你团队的规则 |
| CI 片段 | 入库前拦截 | 见下 |
CI 接入(只要一段):
yaml
- name: 校验 AI 生成的用例
run: |
python scripts/validate_cases.py --swagger swagger.json --cases cases/orders.json
# 退出码 1 = 必须修,直接失败;退出码 2 = 需人工确认,仅告警效果对比:
| 指标 | 无脚本(纯人工) | 有脚本 |
|---|---|---|
| 每批 86 条的人工检查时间 | 约 1.5 小时 | 2 分钟(跑脚本)+ 15 分钟(处理 4 处) |
| 漏检风险 | 高(嵌套字段、相似用例靠肉眼) | 低(规则固定) |
| 可重复性 | 每次标准不一 | 一致 |
下一步可以做的 2 件事:
- 把这 6 条规则反向写进提示词(生成时就不犯),我这边的实测是把"编造字段"从 5 处降到 0 处
- 给
validate_cases.py加--fix模式:能自动修的(删编造字段、合并重复)直接改 JSON,人只看 diff
参考资料
- 字段校验与"分批投喂"做法参考:AI 辅助回归测试的分批投喂与上下文清单
- 断言有效性(不能用恒真断言)参考:Agentic QA Tools
- 变异测试衡量断言质量:Mutation-Guided Unit Test Generation(IEEE TSE 2026)
本文由 曲奇 整理,内容由 AI 辅助整理,示例数据为脱敏演示数据;仅供学习参考,转载请注明出处。