测试左移:需求评审与风险评估¶
一、一次 30 分钟就「通过」的评审,返工了 5 天¶
2026-07 的退款需求评审,会议记录原文(节选):
10:00 PM :需求就这些,支持退款,金额原路退回,大家看下有没有问题。
10:03 Dev :没有,接口加个字段就行。
10:05 QA :退款按钮放哪?
10:06 PM :订单详情页,已支付状态显示。
10:08 QA :好的,没问题。
10:30 PM :那就这样,我发 JIRA 了。
评审结论只有三个字:「已评审」。5 天后联调暴露 3 个问题:
- 部分退款后,订单状态是「已退款」还是「部分退款」没人定义,前端按「已退款」展示,用户以为全额退了。
- 优惠券分摊金额没算,退款金额比用户实付多 10 元,测试环境跑了 12 笔才发现。
- 退款回调重复投递,退款流水写了两条,财务对账差 20 元。
返工成本:3 人 × 5 天 = 15 人日。评审如果按下面的清单走,这三个问题在 10:05 就能被问出来,成本是 1 小时。
左移不是多开会,是把「没写清楚的地方」在写代码前变成待办。
二、评审前 24 小时要做的准备¶
会议现场才开始读需求,必然只能读到「有没有错别字」。准备工作固定四件事:
| 时间点 | 动作 | 产出 |
|---|---|---|
| T-24h | 产品发出需求文档 + 原型 + 接口草稿,标注本次变更点 | 带 [变更] 标记的文档 |
| T-24h | 测试通读,按七维清单逐条打问号 | 问题清单(≥5 条,带原文行号) |
| T-12h | 问题按「阻塞开发 / 影响测试设计 / 可后置」分级 | 分级后的问题列表 |
| T-1h | 把阻塞级问题提前发给产品,能异步解决的先解决 | 会上只剩需要多方拍板的 |
会前问题清单长这样,不要写「需求不清晰」:
Q1 [阻塞开发] 第 3 节「退款金额原路退回」:部分退款时优惠券分摊金额是否退回?
涉及表 refund_order.amount 与 coupon.discount_amount,需要明确计算式。
Q2 [阻塞开发] 第 4 节:退款成功后订单状态取值?枚举目前只有 PAID/CLOSED,
没有 PARTIAL_REFUNDED,需确认是否新增。
Q3 [影响测试] 第 5 节「异步通知」:重试次数与间隔未定义,无法设计幂等用例。
Q4 [可后置] 第 6 节文案「退款将在 1-3 个工作日到账」与旧版「24 小时」冲突。
需求里出现这些词,直接标为「不可测」,要求改写:
模糊词的判定标准:同一句话,两个测试人员能写出不同预期结果,就是不可测。 「界面友好」不可测;「列表加载超过 2 秒显示骨架屏」可测。
三、需求评审检查清单(七个维度)¶
每个维度都要有「检查问句 + 不合格示例 + 必须索取的产出」,否则清单会退化成勾选表演。
1. 可测性¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 每条验收标准能否用「给定-当-则」写出来? | 「退款体验要顺畅」 | 至少 1 条 Gherkin AC |
| 预期结果是否是具体值/状态/文案? | 「提示成功」 | 具体文案 + 状态码 + 字段值 |
| 有没有无法观测的内部行为被当成 AC? | 「异步处理完成」 | 可查询的状态字段或消息日志 |
Feature: 订单部分退款
Scenario: 部分退款金额按实付比例分摊
Given 订单 SO20260701001 实付 90.00,其中券抵扣 10.00
And 订单包含商品 A(60.00)与商品 B(40.00)
When 对商品 A 发起退款
Then 退款金额为 54.00(60.00 × 90.00 / 100.00,四舍五入到分)
And 订单状态为 PARTIAL_REFUNDED
And 优惠券不退回
Gherkin 写法本身就是可测性检查:写不出来,说明需求缺信息。
2. 边界¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 金额的最小单位、最大额度、舍入规则? | 「按实际金额退」 | 分/元单位 + 舍入方式 + 上限 |
| 退款次数、时间窗口? | 「支持多次退款」 | 次数上限 + 时间窗 |
| 0 元订单、全额退、超额退怎么处理? | 未提及 | 三种场景的预期 |
计算类需求的边界一定要落到公式:refund = round(paid × itemAmount / orderAmount, 2, HALF_UP),并明确「多次部分退款后尾差归谁」。这是资损高发点。
3. 异常¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 支付渠道退款失败怎么办? | 「失败重试」 | 重试次数/间隔/最终态 |
| 回调重复、乱序、丢失? | 未提及 | 幂等键 + 乱序处理策略 |
| 退款中用户再次申请? | 未提及 | 互斥规则 + 返回码 |
| 服务重启/消息积压? | 未提及 | 补偿任务 + 对账频率 |
4. 权限¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 谁可以发起退款?客服/运营/财务分别到什么额度? | 「客服可以退」 | 角色-额度矩阵 |
| 越权退款如何拦截? | 未提及 | 鉴权点 + 审批流 |
| 操作日志是否可追溯? | 未提及 | 日志字段(操作人/工单号/金额) |
5. 数据一致性¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 退款单、订单、支付流水、券、库存、积分,哪些字段要同事务更新? | 未提及 | 字段级一致性矩阵 |
| 对账口径是什么?多久跑一次?差异怎么处理? | 「财务会核对」 | 对账文件字段 + 差异工单流程 |
| 部分成功(退 2 件成功 1 件)怎么记? | 未提及 | 明细粒度 + 汇总规则 |
6. 兼容¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 老版本客户端收到新状态枚举会怎样? | 未提及 | 枚举兜底规则 |
| 历史订单没有退款字段,展示如何兜底? | 未提及 | 数据回填脚本或默认值 |
| 接口是否 breaking change? | 「加个字段而已」 | OpenAPI diff 结果 |
1 breaking change(s) detected:
POST /api/v1/orders/{orderNo}/refund
- request property "amount" changed from string to number
这类变更不提前发现,老客户端上线即报 JSON parse error。
7. 回滚¶
| 检查问句 | 不合格示例 | 必须索取的产出 |
|---|---|---|
| 退款上线后出现资损,怎么停? | 「回滚代码」 | 开关名 + 生效范围 + 关闭耗时 |
| 已产生的退款数据要不要回滚? | 未提及 | 数据修复 SQL 或补偿任务 |
| 回滚后新老版本数据兼容? | 未提及 | 双写/兼容期说明 |
评审通过的最低标准:七维中「可测性、边界、异常、数据一致性」四维不得有阻塞级空白;其余三维允许带待办,但必须有 owner 和截止日期。
四、把一段退款需求拆成测试点¶
需求原文(真实文档节选,歧义原样保留):
支持对已支付订单发起退款,退款金额原路返回。支持部分退款,按商品维度退。 退款成功后订单状态同步更新,并通知用户。客服可在后台操作,金额较大的需要审批。 退款失败时自动重试,最终失败进入人工处理。优惠券按规则处理。
逐句拆解:
| # | 原文 | 歧义/缺口 | 必须问的问题 | 测试点 | 风险 |
|---|---|---|---|---|---|
| 1 | 已支付订单发起退款 | 已发货/已完成能退吗?退款中能再退吗? | 哪些状态允许退款 | 状态准入矩阵(PAID/SHIPPED/COMPLETED/CLOSED × 退款) | 高 |
| 2 | 退款金额原路返回 | 原路是哪个渠道?混合支付(券+余额+微信)怎么分? | 多支付方式拆分规则 | 混合支付退款金额分配、渠道退款单号回填 | 极高 |
| 3 | 支持部分退款,按商品维度退 | 数量维度?同商品分两次退? | 退款粒度与次数上限 | 单品/多品/同一商品部分数量/多次退款累计 | 高 |
| 4 | 退款成功后状态同步更新 | 部分退款的状态枚举未定义 | 新增状态值与展示文案 | PARTIAL_REFUNDED 的状态流转与列表筛选 | 高 |
| 5 | 通知用户 | 渠道(短信/推送/站内信)与失败补偿 | 通知模板、重试 | 通知发送与去重、通知失败不影响退款主流程 | 中 |
| 6 | 客服后台操作 | 权限边界、可退额度 | 角色-额度矩阵 | 越权、超额度、跨商户操作 | 极高 |
| 7 | 金额较大需要审批 | 「较大」未量化 | 阈值 + 审批人 | 阈值边界(9999.99/10000/10000.01)、审批拒绝后状态 | 高 |
| 8 | 失败自动重试 | 次数、间隔、超时、幂等 | 重试策略 | 重复回调只退一次、重试达上限转人工、乱序回调 | 极高 |
| 9 | 最终失败进入人工处理 | 人工处理入口与状态 | 工单字段 | 人工补退、人工关单、对账差异 | 中 |
| 10 | 优惠券按规则处理 | 规则未定义;部分退款是否退券 | 券分摊与退回规则 | 全额退退券/部分退不退券/券已过期 | 极高 |
10 行里有 6 个「必须问」,这就是评审要产出的东西。没有这 6 个答案,用例设计只能靠猜,猜错的概率和资损金额成正比。
把答案写成可执行 AC(示例 2 条):
Scenario: 金额等于审批阈值时不触发审批
Given 客服角色额度上限为 10000.00
When 发起退款金额 10000.00
Then 退款单直接进入 PROCESSING
And 不生成审批工单
Scenario: 金额超过阈值 1 分触发审批
Given 客服角色额度上限为 10000.00
When 发起退款金额 10000.01
Then 生成审批工单,状态为 PENDING_APPROVAL
And 退款不发起渠道请求
五、风险矩阵:先评分,再决定投入¶
概率 P 与影响 I 都取 1~5,得分 = P × I。矩阵如下(分数写在格子里):
| 影响 \ 概率 | 1 极低 | 2 低 | 3 中 | 4 高 | 5 极高 |
|---|---|---|---|---|---|
| 5 灾难(资损/合规) | 5 | 10 | 15 | 20 | 25 |
| 4 严重(核心流程阻断) | 4 | 8 | 12 | 16 | 20 |
| 3 中等(功能受损有绕过) | 3 | 6 | 9 | 12 | 15 |
| 2 轻微(体验受损) | 2 | 4 | 6 | 8 | 10 |
| 1 可忽略(文案) | 1 | 2 | 3 | 4 | 5 |
分档规则(写死,避免每次评审重新吵):
| 分值 | 等级 | 处置 |
|---|---|---|
| 15~25 | 极高 | 必须有专项测试(安全/性能/故障注入)+ 灰度 + 回滚演练,测试负责人签字 |
| 10~14 | 高 | 全量用例 + 自动化回归 + 上线后 24 小时观察 |
| 5~9 | 中 | 常规用例 + 变更点回归 |
| 1~4 | 低 | 抽测或随版本验证 |
附加规则:影响 I=5 时,无论概率多低,等级不得低于「高」。资损类问题不接受「概率低所以不测」。
退款需求的评分示例:
| 风险项 | P | I | 分值 | 等级 | 对应动作 |
|---|---|---|---|---|---|
| 混合支付退款金额拆分错误 | 4 | 5 | 20 | 极高 | 专项用例 18 条 + 财务对账演练 |
| 重复回调导致重复退款 | 3 | 5 | 15 | 极高 | 幂等测试 + 故障注入(重放 100 次) |
| 审批阈值边界判断错误 | 3 | 4 | 12 | 高 | 边界用例 6 条 + 权限矩阵 |
| 部分退款状态展示错误 | 4 | 2 | 8 | 中 | 前端用例 + 老版本兼容 |
| 退款通知文案不符 | 3 | 1 | 3 | 低 | 抽测 |
评分表可以直接用表格工具算,公式(假设 P 在 B 列、I 在 C 列):
注意 OR 分支要放最前,否则 I=5、P=1 的项会被算成「中」。
六、FMEA:S/O/D 与 RPN 计算¶
风险矩阵看「需求整体」,FMEA 看「单个失效模式」。三个维度都取 1~10:
| 维度 | 含义 | 1 分 | 5 分 | 10 分 |
|---|---|---|---|---|
| S 严重度 | 失效后果 | 无感知 | 功能受损可绕过 | 资损/数据丢失/合规事故 |
| O 发生度 | 失效发生概率 | 几乎不发生(<1/10000) | 偶发(1/1000) | 高频(>1/10) |
| D 探测度 | 现有手段发现难度 | 必现且监控可即时告警 | 需要专项用例才能发现 | 上线后靠用户投诉 |
RPN = S × O × D,取值 1~1000。处置阈值:RPN ≥ 100,或 S ≥ 8,必须制定预防/探测措施;措施后重新评分,记录改善值。
退款需求 FMEA 示例:
| 失效模式 | 后果 | S | O | D | RPN | 措施 | 措施后 S/O/D | 新 RPN |
|---|---|---|---|---|---|---|---|---|
| 混合支付拆分比例算错 | 多退/少退 | 9 | 4 | 6 | 216 | 用实付比例计算 + 财务口径 AC + 对账断言 | 9/2/3 | 54 |
| 回调重放重复退款 | 重复出款 | 10 | 3 | 7 | 210 | 退款单号做唯一键 + 状态机幂等 | 10/1/2 | 20 |
| 部分退款状态未同步 | 用户重复申请 | 6 | 4 | 5 | 120 | 同事务更新 + 状态一致性巡检 | 6/2/2 | 24 |
审批阈值用 > 而非 >= |
该审批未审批 | 8 | 3 | 4 | 96(S≥8 仍需处理) | 边界用例 + 单元测试参数化 | 8/1/2 | 16 |
| 通知失败阻断主流程 | 退款卡单 | 7 | 3 | 3 | 63 | 通知异步化 + 失败落表 | 7/2/2 | 28 |
O 和 D 不能拍脑袋,要有数据来源:
- O 用历史缺陷数/调用量估算。例:退款相关缺陷近 12 个月 14 个,退款调用 2.1 万次 → 约 1/1500,对应 O=5 附近。
- D 用现有用例与监控覆盖反推。有自动化断言且告警覆盖 = 2~3;只能手工专项测 = 5~6;线上投诉才发现 = 8~10。
常见误用:
| 误用 | 后果 | 正确做法 |
|---|---|---|
| 所有项 O 都填 5、D 都填 5 | RPN 全部集中在 125,没有区分度 | O 用历史数据,D 用覆盖证据,并在表格里写依据 |
| 只看 RPN 排序 | 高 S、低 RPN 的资损项被排到后面 | 加 S≥8 的一票否决规则 |
| 措施写「加强测试」 | 无法验证是否生效 | 措施必须是可验证动作:新增哪条用例/哪个唯一键/哪个告警 |
| 评完不更新 | 半年后阈值失效 | 每次需求变更或线上故障后复评,保留历史版本 |
七、需求变更影响分析¶
开发第 3 天提出变更:「产品说要支持部分退款,接口加个 items 数组」。
变更影响分析表(评审必填):
| 变更点 | 受影响模块 | 关联用例 | 需回归范围 | 工作量 | 风险 | 决策 |
|---|---|---|---|---|---|---|
| 退款接口支持按商品明细退 | refund-service / order-service / 财务对账 | TC-RF-01~18 | 退款全流程、订单金额、对账 | 3 人日 | 极高 | 本迭代做,砍掉「退款通知模板改版」 |
| 订单状态新增 PARTIAL_REFUNDED | 订单列表/详情/筛选/报表 | TC-OD-22~31 | 订单查询、导出、BI 报表 | 2 人日 | 高 | 本迭代做,需数据回填脚本 |
| 券分摊金额计算 | coupon-service | TC-CP-40~46 | 券核销、退款、对账 | 2 人日 | 极高 | 本迭代做,财务确认口径 |
| 后台审批流 | admin-web | 无(新功能) | 权限、审批 | 3 人日 | 高 | 延期到下迭代,先上线人工工单 |
找出「改了哪些代码」不能靠回忆:
# 本次变更涉及的文件
git diff --name-only origin/release/2.3.0...HEAD
# 退款相关历史变更,判断影响面
git log --oneline --since="6 months ago" --grep="refund" -30
# 具体改动量
git diff --stat origin/release/2.3.0...HEAD -- src/refund src/order
src/refund/RefundService.java | 142 ++++++++++++++++-----
src/refund/RefundCalculator.java | 88 ++++++++++---
src/order/OrderStatus.java | 12 ++-
3 files changed, 198 insertions(+), 44 deletions(-)
变更评审只问三句:
- 改了什么:字段、枚举、状态机、接口签名,逐项列出。
- 不改什么:明确本次不动的范围,防止「顺手重构」扩大回归面。
- 怎么验证:新增哪些用例、回归哪些旧用例、谁来执行、什么时候能测。
第三问没有答案,变更就不该进本迭代。
八、左移在各阶段的动作与产出物¶
| 阶段 | 测试动作 | 产出物 | 门禁(不通过会怎样) |
|---|---|---|---|
| 需求 | 七维清单评审、AC 可测性检查、风险矩阵 | 问题清单、AC 列表、风险等级 | 阻塞级问题未闭环,需求不进开发排期 |
| 设计 | 评审接口契约、状态机、数据模型、异常码 | OpenAPI diff、状态迁移表、错误码表 | breaking change 未声明,接口不准提测 |
| 编码 | 单测覆盖增量、静态扫描、契约测试 | 覆盖率报告、Sonar 结果 | 增量覆盖率 <60%,CI 卡住合并 |
| 提测 | 冒烟、准入检查、环境校验 | 冒烟报告、提测单 | 冒烟失败,构建打回 |
| 发布 | 回归、灰度、回滚演练、监控确认 | 测试报告、发布检查单 | P0/P1 未清零,禁止发布 |
| 线上 | 观察黄金指标、逃逸缺陷登记 | 24 小时观察记录、逃逸分析 | 指标越界,触发回滚 |
每一条都要有工具落地,不能只写「加强」:
# 编码阶段:单测 + 覆盖率门槛,CI 里执行
mvn -q test jacoco:check -Djacoco.minimum=0.60
# 契约阶段:接口变更检测
oasdiff breaking old.yaml new.yaml
# 提测阶段:冒烟门禁,失败直接非零退出
pytest -m smoke --maxfail=1 -q || exit 1
九、评审会怎么开¶
时间盒(60 分钟)¶
| 时长 | 内容 | 产出 |
|---|---|---|
| 5 分钟 | 产品讲背景与本次变更点,不讲全文 | 变更清单确认 |
| 40 分钟 | 逐条过会前问题清单,每条不超过 3 分钟 | 决议:通过/修改/待定 |
| 10 分钟 | 过风险矩阵,确认测试策略与资源 | 风险等级与测试投入 |
| 5 分钟 | 确认待办、owner、截止时间 | 会议纪要 |
超过 60 分钟还没结论的条目,一律转为「会后专项」,不占用全场时间。
角色分工¶
| 角色 | 必须回答的问题 |
|---|---|
| 产品 | AC 是什么?异常场景的业务预期是什么?优先级怎么排? |
| 开发 | 涉及哪些表和服务?有无 breaking change?异常码怎么定义? |
| 测试 | 哪些点不可测?需要什么数据/权限/工具?风险最高的三项是什么? |
| 架构/运维 | 有无容量与依赖风险?回滚路径是什么?监控覆盖哪些指标? |
输出模板(可直接复制)¶
# 需求评审纪要:订单部分退款(v1.2)
## 结论
- [ ] 通过(可进入开发)
- [x] 有条件通过:3 个阻塞问题需在 07-15 前闭环
- [ ] 不通过
## 阻塞问题
| # | 问题 | Owner | 截止 | 状态 |
| --- | --- | --- | --- | --- |
| 1 | 混合支付退款拆分规则 | PM | 07-12 | 已闭环 |
| 2 | 部分退款状态枚举与展示 | PM + 前端 | 07-13 | 进行中 |
| 3 | 券分摊金额计算式 | 财务 | 07-15 | 未开始 |
## 风险与测试策略
| 风险 | 等级 | 策略 |
| --- | --- | --- |
| 重复退款 | 极高 | 幂等专项 + 回调重放 100 次 |
| 金额拆分 | 极高 | 18 条专项用例 + 对账演练 |
## 待办
- [ ] 开发补充错误码表(Owner:@dev-li,07-16)
- [ ] 测试补充退款专项用例(Owner:@qa-wang,07-17)
评审有效性怎么度量¶
| 指标 | 公式 | 目标 | 数据来源 |
|---|---|---|---|
| 需求缺陷密度 | 评审发现问题数 / 需求页数 | ≥1.5 条/页 | 评审纪要 |
| 评审逃逸率 | 开发阶段才发现的歧义数 / 评审问题总数 | ≤20% | JIRA label=需求评审 |
| 变更返工率 | 因需求不清导致的返工任务数 / 总任务数 | ≤5% | 迭代看板 |
| 评审闭环率 | 截止前闭环的阻塞问题数 / 阻塞问题总数 | 100% | 纪要待办 |
project = QALAB AND labels = "需求评审" AND created >= startOfQuarter()
project = QALAB AND labels = "返工" AND text ~ "需求未定义"
如果评审逃逸率连续两个迭代 >20%,问题不在测试,而在问题清单没有分级、阻塞项没有截止日期。
十、检查清单¶
- 会前 24 小时拿到文档、原型、接口草稿,变更点有标记
- 会前问题 ≥5 条,带原文行号,已按阻塞/影响/后置分级
- 七维清单逐条过,重点确认可测性、边界、异常、数据一致性
- 每条 AC 能写成 Given/When/Then,且有具体数值或状态
- 金额类需求写明单位、舍入方式、尾差归属
- 状态机新增状态时,确认枚举兜底与历史数据展示
- 接口变更跑过 oasdiff,breaking change 已声明
- 权限需求有角色-额度矩阵,越权有明确返回码
- 风险矩阵评分有依据,I=5 的项不低于「高」
- FMEA 只对高 S 或高 RPN 项做,措施可验证
- 变更影响分析表填了回归范围与工作量
- 评审纪要含结论、阻塞项、owner、截止时间
- 阻塞项未闭环时,需求不进入开发排期