跳转至

08 质量保证与 Word 验收报告:用证据代替“我觉得可以了”

质量保证的目标不是“证明 AI 写对了”,而是 用尽可能低的成本,尽快发现它写错了。本章的核心方法:Chrome MCP 做端到端验证,步骤截图 + manifest 做证据链,Word 报告做人类可读的验收界面。


1. 质量保证的四层防线

第一层:编译器 / 类型检查 / lint
   ↓ 消灭语法、类型、明显静态错误
第二层:单元测试 + 集成测试
   ↓ 消灭业务逻辑、边界、数据库约束错误
第三层:契约测试 + API 测试
   ↓ 消灭前后端字段/错误格式不一致
第四层:E2E + 人工走查
   ↓ 消灭真实用户流程、视觉、体验问题

AI 可以负责前三层的执行和修复,但第四层的“体验判断”必须由人做。


2. 测试策略回顾

类型 谁写 跑在哪 门禁
单元测试 功能 Agent 本地/CI 必须绿,覆盖率达标
集成测试 功能 Agent 本地 Docker/CI 必须绿
API/契约测试 功能 Agent + 集成 Agent CI 必须绿
E2E 测试 Agent 本地/预发/生产 必须绿 + 有截图
人工走查 人 本地/预发 必须签字

不要用 E2E 替代单元测试。 E2E 慢、脆、难定位;单元测试快、稳、定位准。E2E 用来验证“用户能不能完成目标”,不是用来验证每个分支。


3. Chrome MCP 的两种用法

3.1 用“文本 + JS”理解页面

这是 AI 测试的主要方式,token 消耗低、结果准确:

工具 用途 示例
chrome_navigate 打开 URL http://localhost:5173/login
chrome_read_page 获取可访问性树/可见文本 找按钮、输入框、表格
chrome_javascript 执行 JS,返回 JSON 断言 URL、文本、localStorage、DOM 状态
chrome_click_element 点击元素 按 ref 或 selector
chrome_fill_or_select 填表单 输入框、下拉框
chrome_console 读控制台 检查错误
chrome_network_capture 抓网络 检查 API 状态码/响应
chrome_screenshot 截图 只保存文件,不回读模型

3.2 用“截图”留证

截图的作用是 给人看,不是给 AI 看。正确做法:

chrome_screenshot(
  savePng: true,
  storeBase64: false,
  fullPage: false,
  name: "docs/04-qa/screens/US-004/step-03.png"
)
  • storeBase64: false:不要把图片塞回模型上下文。
  • savePng: true:保存到磁盘。
  • 文件名带故事 ID 和步骤号。
  • 默认每个步骤都截图,因为 storeBase64: false 只写文件,token 成本很低;截图是给人看的证据,不是给模型看的输入。
  • 如果步骤非常密集(例如连续 20 次输入),可以把同一验证点的多个微操作合并为一个“可验收步骤”,但每个步骤都应有截图。

如果必须让模型“看”截图(例如判断视觉错位),再单独读取,并限制频率。优先用 chrome_javascript 读 computed style 和布局。

3.3 两种浏览器模式

模式 用途 启动
有头 + 持久 profile 需要人工登录、验证码、2FA ./scripts/start-chrome-debug.sh
无头 CI/自动化 CHROME_HEADLESS=1 ./scripts/start-chrome-debug.sh
连接 Windows 宿主机 Chrome 复用你的登录态 见 05 开发环境搭建

推荐:本地验收用有头模式,人工先登录一次;CI 用无头 + 测试账号/token 注入。


4. E2E 测试协议

4.1 测试前准备

  • 应用已启动(前端 + 后端 + 数据库)
  • 测试账号可用,权限正确
  • 种子数据已创建(带唯一前缀)
  • Chrome 调试端口可用
  • 已记录环境信息(URL、commit、镜像版本)
  • 已清空上一次的截图目录(或使用新 run_id)

4.2 测试数据命名

e2e-<run_id>-<story_id>-<purpose>
例如:e2e-20261030T1420-US004-task-001

好处:

  • 容易筛选;
  • 容易清理;
  • 不会和真实数据混淆;
  • 多个 Agent 并行不冲突。

4.3 每一步的协议

每个步骤必须包含:

  1. 动作:做了什么(navigate/click/fill/assert)。
  2. 预期:应该发生什么(来自验收标准)。
  3. 实际:实际发生了什么(JS 返回值/文本)。
  4. 证据:截图路径(每一步)。
  5. 结果:pass/fail。
  6. 错误:控制台/网络错误(如果有)。

4.4 断言优先用 JS

// 断言 URL
({ path: location.pathname, search: location.search })

// 断言元素文本
document.querySelector('[data-testid="task-title"]')?.textContent?.trim()

// 断言元素存在
!!document.querySelector('[data-testid="create-task-modal"]')

// 断言按钮禁用
document.querySelector('button[type="submit"]')?.disabled

// 断言表单错误
document.querySelector('[data-testid="title-error"]')?.textContent

// 断言列表数量
document.querySelectorAll('[data-testid="task-card"]').length

// 断言 localStorage 有 token(不要返回 token 本身)
!!localStorage.getItem("access_token")

// 断言网络请求(Performance API)
performance.getEntriesByType("resource")
  .filter(e => e.name.includes("/api/v1/tasks"))
  .map(e => ({ name: e.name, duration: e.duration }))

4.5 填写 Vue/React 受控输入

直接设置 value 可能不会触发框架的更新。用原生 setter + input 事件:

const el = document.querySelector('input[name="title"]');
const setter = Object.getOwnPropertyDescriptor(
  HTMLInputElement.prototype, "value"
).set;
setter.call(el, "e2e-20261030-US004 测试任务");
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));

下拉框:

const select = document.querySelector('select[name="priority"]');
const setter = Object.getOwnPropertyDescriptor(
  HTMLSelectElement.prototype, "value"
).set;
setter.call(select, "high");
select.dispatchEvent(new Event("change", { bubbles: true }));

4.6 等待

不要用固定 sleep。用轮询:

async function waitFor(fn, timeout = 5000, interval = 100) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    try {
      const v = fn();
      if (v) return v;
    } catch {}
    await new Promise(r => setTimeout(r, interval));
  }
  throw new Error("timeout");
}
await waitFor(() => document.querySelector('[data-testid="task-card"]'));

4.7 控制台与网络

  • 用 chrome_console 的 buffer 模式,测试开始前清空,结束时读取错误。
  • 用 chrome_network_capture 抓 API 请求,检查:
  • 状态码(不能有 5xx);
  • 响应结构;
  • 耗时;
  • 是否有重复请求。
  • 断言时不要把整个响应体贴进上下文,只取关键字段。

4.8 一个完整的 E2E 提示词

你是 E2E 测试工程师。请用 Chrome MCP 验证 US-004“创建任务”。

环境:
- 前端:http://localhost:5173
- 测试账号:e2e-user@example.com / <从环境变量读取,不要写进报告>
- 测试数据前缀:e2e-20261030T1420-US004

步骤:
1. 打开登录页,登录测试账号。
2. 进入项目 “e2e-20261030T1420-US004-project”。
3. 点击“新建任务”。
4. 只填标题,不填负责人,提交。断言显示校验错误。
5. 填写标题、负责人、截止日期,提交。断言:
   - 返回 201
   - 弹窗关闭
   - 看板出现新卡片
   - 卡片标题正确
6. 退出登录,用无权限账号登录,尝试创建任务。断言 403 提示。
7. 每一步:
   - 每一步截图保存到 docs/04-qa/screens/US-004/(只保存文件,不回读模型)
   - 用 chrome_javascript 断言,不要靠截图猜
   - 把动作/预期/实际/截图路径写入 manifest
8. 结束时读取控制台错误和网络 5xx,写入 manifest。
9. 生成 docs/04-qa/manifests/US-004.json。
10. 输出通过/失败的步骤摘要,不要贴大段 HTML。

约束:
- 不要在生产环境测试写操作(本步骤在 dev)。
- 不要删除其他测试数据。
- 截图只保存,不要回读模型。
- 遇到失败,保留现场截图和错误信息,不要继续瞎点。

5. Manifest:E2E 的机器可读记录

docs/04-qa/manifests/US-004.json 是 Word 报告的数据源:

{
  "run_id": "20261030T1420",
  "story_id": "US-004",
  "title": "创建任务",
  "environment": "dev",
  "base_url": "http://localhost:5173",
  "api_base_url": "http://localhost:8080/api/v1",
  "commit": "abc1234",
  "account": "e2e-user@example.com",
  "started_at": "2026-10-30T14:20:00+08:00",
  "finished_at": "2026-10-30T14:26:12+08:00",
  "result": "fail",
  "console_errors": [
    {
      "step": 5,
      "type": "error",
      "text": "Uncaught TypeError: Cannot read properties of undefined"
    }
  ],
  "network_errors": [
    {
      "step": 5,
      "method": "POST",
      "url": "/api/v1/tasks",
      "status": 500,
      "request_id": "01J..."
    }
  ],
  "steps": [
    {
      "index": 1,
      "action": "navigate",
      "target": "http://localhost:5173/login",
      "expected": "登录页加载",
      "actual": "登录页加载,标题为“登录”",
      "status": "pass",
      "screenshot": "screens/US-004/step-01-login.png",
      "assertions": [
        {
          "type": "js",
          "expression": "document.title",
          "expected": "登录 - TeamTask",
          "actual": "登录 - TeamTask"
        }
      ],
      "notes": ""
    },
    {
      "index": 5,
      "action": "click",
      "target": "[data-testid=submit-task]",
      "expected": "任务创建成功,弹窗关闭,看板出现卡片",
      "actual": "接口返回 500,弹窗未关闭",
      "status": "fail",
      "screenshot": "screens/US-004/step-05-fail.png",
      "assertions": [
        {
          "type": "network",
          "expression": "POST /api/v1/tasks",
          "expected": "201",
          "actual": "500"
        }
      ],
      "notes": "后端日志 request_id=01J..."
    }
  ],
  "defects": [
    {
      "id": "BUG-004-01",
      "severity": "P1",
      "summary": "创建任务时 500",
      "expected": "201",
      "actual": "500",
      "evidence": "screens/US-004/step-05-fail.png",
      "status": "open"
    }
  ]
}

5.1 Manifest 规则

  • 一个故事一个文件。
  • 步骤编号从 1 开始。
  • status 只能是 pass / fail / skip。
  • 截图路径相对于 docs/04-qa/。
  • 不记录密码、令牌、身份证等敏感信息。
  • result 是所有步骤的汇总:有 fail 就是 fail。
  • 运行结束后立即写入,不要等报告生成时再补。

6. Word 验收报告

6.1 为什么用 Word

  • 人可以像审文档一样审流程,不用来回点浏览器;
  • 截图 + 步骤 + 预期/实际在一页里,容易判断;
  • 可以打印、批注、签字;
  • 比纯文本日志更适合非技术评审人;
  • 出问题时是完整的证据链。

6.2 报告结构

封面
  标题:US-004 创建任务 - 开发环境 E2E 验收报告
  运行 ID / 环境 / 时间 / 结果

1. 概要
   - 测试范围
   - 通过/失败统计
   - 结论

2. 环境信息
   - 前端/后端 URL
   - commit / 镜像版本
   - 浏览器版本
   - 测试账号(脱敏)

3. 验收标准覆盖
   - AC-1 ~ AC-4,对应步骤,结果

4. 测试步骤(每一步)
   - 步骤号 / 动作 / 预期 / 实际 / 结果
   - 截图
   - 断言结果
   - 错误信息(如有)

5. 控制台与网络错误
   - 错误 / 步骤 / request_id

6. 缺陷清单
   - ID / 严重度 / 描述 / 状态 / 修复提交

7. 结论与签字
   - 是否通过
   - 待办
   - 评审人 / 日期

附录
   - 请求 ID 列表
   - 测试数据清理记录

6.3 生成脚本

完整脚本见 脚本/build_test_report.py。核心逻辑:

from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
import json
from pathlib import Path

def set_cn_font(run, name="Microsoft YaHei"):
    run.font.name = name
    run._element.rPr.rFonts.set(qn("w:eastAsia"), name)

def build_report(manifest_path: Path, out_path: Path, qa_root: Path):
    data = json.loads(manifest_path.read_text(encoding="utf-8"))
    doc = Document()

    # 标题
    title = doc.add_heading(f"{data['story_id']} {data['title']} - E2E 验收报告", level=0)
    for run in title.runs:
        set_cn_font(run)

    # 概要表
    table = doc.add_table(rows=0, cols=2)
    table.style = "Light List Accent 1"
    for k, v in [
        ("运行 ID", data["run_id"]),
        ("环境", data["environment"]),
        ("结果", data["result"]),
        ("开始时间", data["started_at"]),
        ("结束时间", data["finished_at"]),
        ("Commit", data.get("commit", "")),
        ("前端", data["base_url"]),
        ("测试账号", data.get("account", "")),
    ]:
        row = table.add_row().cells
        row[0].text = k
        row[1].text = str(v)

    # 步骤
    doc.add_heading("测试步骤", level=1)
    for step in data["steps"]:
        doc.add_heading(
            f"步骤 {step['index']}:{step['action']} - {step['status'].upper()}",
            level=2,
        )
        for label, value in [
            ("目标", step.get("target", "")),
            ("预期", step.get("expected", "")),
            ("实际", step.get("actual", "")),
        ]:
            p = doc.add_paragraph()
            r = p.add_run(f"{label}:")
            r.bold = True
            set_cn_font(r)
            r2 = p.add_run(str(value))
            set_cn_font(r2)

        # 断言表
        if step.get("assertions"):
            t = doc.add_table(rows=1, cols=4)
            t.style = "Light Grid Accent 1"
            hdr = t.rows[0].cells
            for i, h in enumerate(["类型", "表达式", "预期", "实际"]):
                hdr[i].text = h
            for a in step["assertions"]:
                cells = t.add_row().cells
                cells[0].text = a.get("type", "")
                cells[1].text = str(a.get("expression", ""))
                cells[2].text = str(a.get("expected", ""))
                cells[3].text = str(a.get("actual", ""))

        # 截图
        if step.get("screenshot"):
            img = qa_root / step["screenshot"]
            if img.exists():
                doc.add_picture(str(img), width=Inches(6.2))
                cap = doc.add_paragraph(f"图:{step['screenshot']}")
                cap.alignment = WD_ALIGN_PARAGRAPH.CENTER

    doc.save(out_path)

6.4 中文字体

python-docx 默认字体对中文不友好。必须设置:

from docx.oxml.ns import qn

def set_style_cn(style, name="Microsoft YaHei"):
    style.font.name = name
    style._element.rPr.rFonts.set(qn("w:eastAsia"), name)

对正文、标题、表格都设置一遍。否则在 Windows 上可能显示为宋体/回退字体,影响阅读。

6.5 截图尺寸

  • 宽度 6.2 英寸 适合 A4 纵向。
  • 整页截图可能很长,建议:
  • 每一步截视口;
  • 长页面用 fullPage: true,但在 Word 中按比例缩小;
  • 或者截取关键元素(selector),但要在 manifest 中注明截取范围。
  • 图片过多会让 Word 文件很大,建议单份报告 < 30MB。

6.6 报告命名

docs/04-qa/
├── e2e-report-dev-20261030.docx
├── e2e-report-staging-20261102.docx
└── e2e-report-prod-20261105.docx

或者按故事:

docs/04-qa/reports/US-004-dev-20261030.docx

一个环境一份总报告更适合人工走查;一个故事一份更适合定位问题。可以两者都生成。


7. AI 修复循环

7.1 发现失败后

  1. 保留现场:截图、控制台、网络、request_id。
  2. 定位:是前端、后端、数据还是环境问题?
  3. 写缺陷:docs/04-qa/defects.md。
  4. 修复:让对应 Agent 修,不要测试 Agent 顺手改业务代码。
  5. 补回归测试:修 bug 必须加一个会失败的测试。
  6. 重跑:只重跑失败的故事,最后跑全量。
  7. 重新生成 Word 报告。

7.2 缺陷记录模板

## BUG-004-01 创建任务返回 500

- 故事:US-004
- 严重度:P1
- 发现步骤:5
- 环境:dev
- 复现步骤:
  1. 登录 e2e 账号
  2. 进入测试项目
  3. 新建任务,填写标题/负责人/日期
  4. 提交
- 预期:201,任务出现在看板
- 实际:500,弹窗未关闭
- 证据:screens/US-004/step-05-fail.png
- request_id:01J...
- 日志:`failed to insert task: null value in column "priority"`
- 根因:前端未传 priority,后端默认值未生效
- 修复:后端在 usecase 层补默认值;前端补必填校验
- 修复提交:def5678
- 回归测试:TestCreateTask_DefaultPriority
- 状态:fixed

7.3 修复提示词

你是后端工程师。请修复 BUG-004-01。

输入:
- 缺陷:docs/04-qa/defects.md#bug-004-01
- 失败 manifest:docs/04-qa/manifests/US-004.json
- 截图:docs/04-qa/screens/US-004/step-05-fail.png
- 后端日志:<粘贴关键日志,不要全量>

要求:
1. 先写一个能复现失败的测试。
2. 修复根因,不要只改表面。
3. 运行相关测试 + 全量测试。
4. 更新缺陷状态和修复提交。
5. 不要修改无关文件。
6. 输出根因、修复、测试结果。

8. 人工走查

8.1 为什么人还要走一遍

AI 的 E2E 擅长验证“功能是否按预期工作”,但不擅长判断:

  • 这个交互是否别扭;
  • 文案是否清楚;
  • 视觉是否舒服;
  • 性能是否可接受;
  • 异常情况下用户是否知道怎么办;
  • 是否符合业务直觉。

8.2 走查方式

推荐 按 Word 报告走,而不是直接打开浏览器乱点:

  1. 打开 e2e-report-dev.docx;
  2. 从封面看结论;
  3. 逐页看步骤、截图、断言;
  4. 对可疑步骤,再打开浏览器复现;
  5. 在 Word 里批注问题;
  6. 把问题整理成缺陷,交给 AI 修;
  7. 修完后重跑并生成新报告。

8.3 走查清单

  • 主流程能走通
  • 每一步的截图和预期一致
  • 错误提示清楚、可恢复
  • 空状态/加载态不突兀
  • 文案没有错别字/歧义
  • 视觉与设计稿一致
  • 性能可接受(页面不卡、请求不慢)
  • 权限行为符合预期
  • 没有控制台错误/5xx
  • 测试数据没有污染

8.4 走查后的签字

## 验收结论

- 验收人:<姓名>
- 日期:2026-11-11
- 结论:通过 / 有条件通过 / 不通过
- 遗留问题:BUG-xxx(P2,可上线后修)
- 签字:________

9. 质量门禁(G6)

进入部署前,必须满足:

  • 所有 P0/P1 故事的 E2E 通过
  • 每一步有截图,manifest 完整
  • Word 报告已生成且可读
  • 控制台无未处理错误
  • 网络无 5xx
  • 缺陷:P0/P1 全部修复,P2 有明确计划
  • 修复的 bug 有回归测试
  • 单元/集成测试全绿
  • 覆盖率达标
  • 人已按 Word 报告走查并签字

没有签字,不部署。


10. 常见问题

问题 1:截图太多,token 爆炸

原因:每一步都 storeBase64: true,图片被读回上下文。

修正:

  • storeBase64: false,只保存文件;
  • 每一步都截图,但只保存文件、不回读模型;步骤过密时合并微操作;
  • 用 JS 断言代替“看图判断”;
  • 需要看图时,单独读取一张,并限制频率。

问题 2:E2E 很脆,经常失败

原因:

  • 固定 sleep;
  • 选择器依赖样式类;
  • 测试数据不隔离;
  • 登录态过期;
  • 异步请求未等待。

修正:

  • 用 data-testid;
  • 轮询等待元素/条件;
  • 每次运行用唯一数据前缀;
  • 测试前检查登录态;
  • 等待网络空闲或关键请求完成。

问题 3:AI 把失败的步骤标成 pass

原因:断言太弱,或者 AI 想“完成任务”。

修正:

  • 断言必须包含预期值和实际值;
  • 审查 manifest,不只看 result;
  • 用两个 Agent 交叉验证(一个执行,一个审查);
  • 人工抽查关键步骤。

问题 4:Word 报告图片不显示

原因:截图路径不对,或脚本运行时工作目录不对。

修正:

  • manifest 中路径相对于 docs/04-qa/;
  • 脚本用绝对路径解析;
  • 生成后检查图片数量;
  • 路径中包含中文时确保 UTF-8。

问题 5:生产环境不敢测

原因:怕污染数据/触发副作用。

修正:

  • 使用专用测试租户;
  • 数据带前缀,测完清理;
  • 只读优先;
  • 写操作使用可撤销的测试数据;
  • 必要时用 feature flag 隔离;
  • 严格遵守生产变更审批。

11. 本章交付清单

  • docs/04-qa/manifests/*.json 完整
  • docs/04-qa/screens/ 截图齐全、命名规范
  • docs/04-qa/defects.md 记录所有缺陷
  • e2e-report-dev.docx 已生成
  • 所有 P0/P1 故事 E2E 通过
  • 人工走查完成并签字
  • CSV 状态更新到 qa_pass
  • 回归测试已补
  • 测试数据已清理

下一章:09 部署与发布 —— 用 CI/CD 和 AWS CLI 把验收通过的版本安全地送上生产。