AI 自动化测试流水线实战(二):Analyst 把 PRD 变 L0/L1 用例套件

0 阅读5分钟

系列第二篇。上一篇讲了流水线的整体编排:5 个子 Agent + 看板驱动。这篇深入第一个真正"干活"的角色——Analyst,看它怎么把一份需求文档,变成一份结构化、可追溯、可确认的测试用例套件。


这篇解决什么问题

大多数 AI 生成用例的方案是:丢给 LLM 一句「帮我写 20 条测试用例」,然后它吐一堆 markdown 给你。问题来了:

  • 无法追溯:这条用例覆盖了需求里的哪一条?说不清。
  • 不可确认:生成一大堆,用户没法快速扫一遍确认"覆盖对不对"。
  • 尺度失控:说好 core,结果写出来一堆边界;说好全量,结果 happy path 都没写全。
  • 无法复用:这次生成的用例,下次换工具/换流程就废了。

Analyst 的目标,就是把「需求文档」加工成一份 带分层、带追溯、带覆盖索引、带确认闸门 的用例套件。


核心分层:L0 / L1 / L2(这是全文重点)

整套用例设计基于一个「分层」思想,产物真相只有一个,其他都是派生。

路径角色谁产生能否手改
L0cases/requirements.json需求原子,溯源用Analyst 解析
L1cases/suite.json用例真相,用户确认闸门Analyst 设计确认前可改
L1 旁路cases/coverage.jsonL0×L1 覆盖索引脚本算出来❌ 禁止手改
L1 视图cases/suite.md只读渲染给人扫一眼脚本渲染
L2specs/*.mdcases/cases-ai.json通道派生,确认后生成脚本/主流程可丢重生

一句话总结铁律:用例真相在 L1(suite.json);L2 的一切都是确认后的派生产物,丢了可以重新生成。

jimeng-2026-08-06-4409-手绘漫画风格的三层金字塔,铅笔线条配淡雅水彩上色。底层绿色方块里画一个文件图标,....png

为什么要分这么细?因为不同产物生命周期完全不同:

  • L0 是溯源——需求变了,能算出哪些用例受污染。
  • L1 是对齐——人是靠这份确认用例方向的。
  • L2 是生产——转成 Playwright spec、扁平 JSON,跑完就完,坏了重生。

Analyst 怎么干活:四步流水

Analyst 是一个标准的子 Agent,它的执行流程是固定的(见 agents/analyst.md):

1. 解析 task 参数(outputDir / reqSource / scope / goal)
2. 加载 skill(test-cases)        // 用例生成规范
3. parse:node helpers/parse-md.js <req.md> → cases/requirements.json   // 需求→L0原子
4. 基于 L0 + scope + goal 设计用例,写入 cases/suite.json              // →L1
5. coverage:node helpers/coverage.js req.json suite.json → coverage.json
6. 渲染只读视图:node helpers/render-suite-md.js suite.json → suite.md
7. 返回 JSON(含统计与覆盖摘要)

它有明确的红线规则,我最喜欢两条:

  • A1. 不编造未写明的 UI:需求没给的按钮文案/路由/接口,用语义占位符(如「登录按钮」),并标注「待页面校验」。禁止假装探索过页面。→ 这防止了"编需求"。
  • A3. 场景原子化:一条 case 只验证一条可判定路径,正向/反向/边界拆开。→ 这防止了"大杂烩用例"。

还有一个硬约束:A6. 无论成功失败,最后输出必须是 JSON,不能有别的文本。跟前一篇说的"强制 JSON 返回"一脉相承。

2. 一条用例长什么样

suite.json 里每条用例是高度结构化的,而非一句自由文本:

{
  "id": "S-003",
  "title": "登录成功-正确凭证",
  "priority": "P0",
  "phase": "readonly",
  "design": {
    "phase": "readonly",
    "automation": "ui"
  },
  "trace": {
    "reqIds": ["R-001", "R-002"],
    "origin": "literal"          // literal=直译 / derived=边界反推 / exploratory=页面补洞
  },
  "steps": [
    { "action": "goto", "target": "/login", "value": "" },
    { "action": "fill", "target": "邮箱", "value": "test@example.com" },
    { "action": "fill", "target": "密码", "value": "Test@123456" },
    { "action": "click", "target": "登录", "value": "" }
  ],
  "expects": [
    { "type": "url", "value": "包含 /dashboard" },
    { "type": "element", "value": "设备总览标题可见" }
  ]
}

几个关键字段:

  • trace.reqIds:这条用例追溯到哪些需求原子(L0)。必须非空——用例不能是"无源之水"。
  • trace.origin:标注用例来源——直译需求 / 边界反推 / 页面补洞。审查时一眼看出哪些是需求的直接映射,哪些是 AI 发挥。
  • expects[].typeurl | text | element | api | data | custom。禁止空 expects,禁止「看起来正常」这种无法断言的预期。

3. 覆盖索引:L0×L1 交叉算覆盖率

Analyst 不算手动脉搏率,它调脚本算。coverage.js 做 L0×L1 的笛卡尔覆盖计算,产出 coverage.json。返回的 summary 会给主线程:

{
  "summary": {
    "reqTotal": 12,        // 需求原子总数
    "casesTotal": 18,      // 用例总数
    "p0": 5,               // P0 用例数
    "coveredReqs": 10,     // 被用例覆盖的需求数
    "uncoveredReqs": 2,    // 漏掉的需求数 ← 审查重点
    "orphanCases": 0       // 无需求来源的孤儿用例
  }
}

uncoveredReqs: 2 这种数字极其重要——它告诉用户"有两条需求没被覆盖"。orphanCases: 0 防止 AI 写一堆不溯源的空用例。这两个数字是覆盖是否完整的第一道体检指标。

4. 确认闸门(这篇最核心)

Analyst 产出的 suite.json 默认是 meta.status: "draft"在用户确认之前,禁止派生任何 L2 产物(红线:不能在确认前调用 to-specs / project-flat)。

主线程会向用户展示覆盖摘要,等用户说「确认」。确认后才执行:

# 1. 标记确认
node helpers/confirm-suite.js cases/suite.json

# 2. 派生平坦用例(给用例源用)
node helpers/project-flat.js cases/suite.json cases/cases-ai.json

# 3. 派生 Playwright 计划
node helpers/to-specs.js cases/suite.json specs/

这套设计有个非常专业的细节:陈旧检测。如果需求文件的 SHA256 变了(meta.reqSourceHash 不一致),套件会被标为 stale,禁止静默覆盖已确认的用例——要先 diff 再重新生成。防止"需求改了,旧用例还在用"的隐性污染。


范围裁剪:scope 决定尺度

Analyst 不是每次都写满用例,而是按 scope 裁剪尺度:

scope覆盖策略
smoke每模块最多 1–2 条 happy path
core关键旅程 + 主要异常
fullcore + 边界 / 权限 / 并发等

这是贴近真实测试管理的做法——冒烟、核心回归、全量回归本就是三种不同尺度的测试活动,不允许用一套尺度糊弄。


抄走什么

这篇的可复用设计:

  1. 分层产物 + 唯一真相源:L0 溯源 / L1 确认闸门 / L2 派生,别把所有东西压在一个文件里。
  2. 用例必须可追溯trace.reqIds 必须非空,审查能算出 uncoveredReqsorphanCases
  3. 不可确认就得卡住:draft 状态在用户确认前禁止派生下游产物;需求变了要标 stale 再 diff,不静默覆盖。

下一篇:用例生成后,测试跑挂了。怎么判断是定位器失效、真 Bug、还是环境问题?Healer 的「归因三分类」和自愈机制,我们下篇见。