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 测试数据命名¶
好处:
- 容易筛选;
- 容易清理;
- 不会和真实数据混淆;
- 多个 Agent 并行不冲突。
4.3 每一步的协议¶
每个步骤必须包含:
- 动作:做了什么(navigate/click/fill/assert)。
- 预期:应该发生什么(来自验收标准)。
- 实际:实际发生了什么(JS 返回值/文本)。
- 证据:截图路径(每一步)。
- 结果:pass/fail。
- 错误:控制台/网络错误(如果有)。
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
或者按故事:
一个环境一份总报告更适合人工走查;一个故事一份更适合定位问题。可以两者都生成。
7. AI 修复循环¶
7.1 发现失败后¶
- 保留现场:截图、控制台、网络、request_id。
- 定位:是前端、后端、数据还是环境问题?
- 写缺陷:
docs/04-qa/defects.md。 - 修复:让对应 Agent 修,不要测试 Agent 顺手改业务代码。
- 补回归测试:修 bug 必须加一个会失败的测试。
- 重跑:只重跑失败的故事,最后跑全量。
- 重新生成 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 报告走,而不是直接打开浏览器乱点:
- 打开
e2e-report-dev.docx; - 从封面看结论;
- 逐页看步骤、截图、断言;
- 对可疑步骤,再打开浏览器复现;
- 在 Word 里批注问题;
- 把问题整理成缺陷,交给 AI 修;
- 修完后重跑并生成新报告。
8.3 走查清单¶
- 主流程能走通
- 每一步的截图和预期一致
- 错误提示清楚、可恢复
- 空状态/加载态不突兀
- 文案没有错别字/歧义
- 视觉与设计稿一致
- 性能可接受(页面不卡、请求不慢)
- 权限行为符合预期
- 没有控制台错误/5xx
- 测试数据没有污染
8.4 走查后的签字¶
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 把验收通过的版本安全地送上生产。