Skip to content

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.py6 项静态校验,返回退出码可直接进 CI相似度阈值、数据准备的团队习惯写法
scripts/case_lint_rules.md6 类问题的判定标准(含反例)补充你团队的规则
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 件事:

  1. 把这 6 条规则反向写进提示词(生成时就不犯),我这边的实测是把"编造字段"从 5 处降到 0 处
  2. 给 validate_cases.py 加 --fix 模式:能自动修的(删编造字段、合并重复)直接改 JSON,人只看 diff

参考资料

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