返回知识库

端到端数据一致性测试实战手册

从用户操作一路检查到数据库、缓存、消息队列和下游系统,证明整条业务链最终说的是同一件事。

10 个章节页面 + API + 数据层Playwright / 数据库 / MQ
🔗

测试目标

页面成功不等于业务成功

端到端数据一致性到底测什么?

页面弹出“操作成功”,只能证明前端收到了成功响应。真正的 E2E 数据一致性测试,还要继续验证业务数据是否在所有相关系统中正确落地。

  • 用户最终看到的状态正确。
  • 接口返回、数据库记录和缓存数据表达同一业务事实。
  • 异步消息被正确消费,下游系统在约定时间内完成处理。
  • 请求重试、消息重复和并发操作不会产生重复业务结果。
  • 中途失败后系统能够重试、补偿、告警和追踪。

贯穿案例

以“使用优惠券的已支付订单被用户取消”为例,你需要验证订单终止履约、库存释放、优惠券只返还一次、退款总额等于实付金额,并且缓存和下游状态最终一致。

🧭

先确定一致性模型

立即正确,还是最终正确

三类一致性

类型业务含义测试方式
强一致操作成功后,相关数据必须立即正确响应返回后立即校验订单状态、金额、权限
最终一致允许短暂不一致,但必须在约定时间内完成在超时时间内轮询优惠券、退款、异步任务
业务一致字段表达不同,但代表的业务事实必须一致订单实付金额必须等于退款流水累计金额

测试开始前必须确认

  • 哪些状态必须立即生效?
  • 哪些状态允许异步完成?
  • 允许不一致多长时间?
  • 超时后由谁重试、补偿或告警?
  • 哪个系统是最终事实来源?
🧾

场景与用例设计

用业务事实组织断言

推荐用例标题

当已支付订单使用优惠券时,取消订单后所有关联数据应在 5 分钟内保持一致

前置数据

  • 创建独立测试用户和库存为 10 的商品。
  • 发放一张 20 元测试优惠券。
  • 创建并支付一笔使用优惠券的订单。
  • 记录订单号、用户 ID、优惠券 ID、支付流水号和初始库存。

操作与结果

  • 从订单详情页发起取消。
  • 立即校验页面、取消接口和订单查询接口。
  • 轮询库存、优惠券、退款和消费记录。
  • 反向校验不存在重复退款、重复返券和多释放库存。

按风险安排执行频率

优先级典型链路执行策略
P0支付、退款、余额、库存、核心订单状态每次发布前执行,任何不一致阻断发布
P1优惠券、积分、会员权益、审批同步主干回归或每日流水线执行
P2低风险异步统计、非关键展示数据定时执行或变更相关时执行
📍

全链路检查点

从入口追到业务终点

订单取消链路检查矩阵

层级正向断言反向断言
页面用户看到订单已取消,操作入口状态正确不能继续发货、支付或重复取消
API查询接口返回取消状态和正确金额HTTP 与业务 code 都正确
订单数据库状态、取消时间、版本号正确只有一次合法状态迁移
库存被占用数量恢复不能少释放或多释放
优惠券恢复可用并留下返还记录返还效果只能发生一次
退款流水退款金额等于实付金额幂等键唯一,无重复退款
缓存不再返回取消前的旧状态回源后不会用旧值覆盖新值
消息与下游取消事件被正确消费重试、死信、补偿均可追踪

统一关联标识

  • orderNo:标识业务订单。
  • traceId:定位一次服务调用。
  • eventId:标识一条业务事件。
  • e2e_run_id:标识整次测试运行,并关联报告、日志与清理任务。
text
e2e_run_id = e2e-20260809-order-cancel-001
order_no  = TEST-ORDER-10001
trace_id  = 6e7d...
event_id  = order-cancelled-10001
⏱️

最终一致的等待策略

轮询状态,不赌固定时间

不要这样等待

固定等待 5 秒既可能浪费时间,也可能在 CI 较慢时误判失败。等待的目标应该是“状态达到预期”,而不是“时间过去了”。

TypeScript / Playwright
await expect
  .poll(
    async () => (await couponApi.get(couponId)).status,
    {
      timeout: 5 * 60 * 1000,
      intervals: [1000, 2000, 5000, 10000],
      message: "订单取消后,优惠券应在 5 分钟内恢复可用",
    },
  )
  .toBe("AVAILABLE");

轮询必须具备

  • 明确的最终状态。
  • 业务认可的最长等待时间。
  • 不会改变业务数据的只读查询。
  • 超时后输出最后状态、等待时长和关联标识。
♻️

幂等、异常与补偿

异常路径才是事故高发区

必须覆盖的故障场景

场景主要风险核心断言
接口超时后重试第一次可能已成功,第二次重复写入同一幂等键只能产生一个业务结果
消息重复消费重复返券、重复退款、库存多释放同一 eventId 的业务效果只发生一次
下游暂时失败主系统成功,下游永远停在旧状态可重试、可补偿、超限后可告警
并发状态操作取消、发货、退款相互覆盖状态机合法,版本冲突被识别
缓存失效失败数据库已更新,用户持续读到旧状态在 SLA 内刷新或回源得到新值

幂等校验不能只看返回码

  • 同一请求重复发送后只有一条业务记录。
  • 同一消息重复消费后余额、库存和权益只变化一次。
  • 幂等键有明确作用域和有效期。
  • 第一次成功但响应丢失时,重试能返回同一业务结果。
🧪

测试数据管理

独立、可追踪、可恢复

数据准备

  • 每条用例使用独立用户、订单和优惠券。
  • 数据带统一 TEST 前缀和 e2e_run_id。
  • 使用测试租户、测试支付渠道和可控库存。
  • 测试之间不依赖执行顺序。

数据清理

  • 优先使用业务允许的测试接口回收数据。
  • 用例失败时先保存证据,再执行清理。
  • 财务流水和审计日志使用冲正或专用测试账本,不直接删除。
  • 清理失败必须告警,避免污染后续测试。
🧰

自动化工程结构

页面操作与数据探针分层
目录结构
tests/
  order-cancel-consistency.spec.ts
fixtures/
  order-scenario.ts
clients/
  order-api.ts
  coupon-api.ts
probes/
  database-probe.ts
  cache-probe.ts
  event-probe.ts
assertions/
  consistency-assertions.ts
TypeScript / Playwright
test("当已支付订单使用优惠券时,取消后关联数据应保持一致", async ({ page }) => {
  const scenario = await orderScenario.createPaidOrderWithCoupon();

  await orderPage.open(scenario.orderNo);
  await orderPage.cancelOrder("不想要了");

  await consistency.expectOrderCancelled(scenario);
  await consistency.expectInventoryReleased(scenario);
  await consistency.expectCouponReturnedOnce(scenario);
  await consistency.expectRefundCompletedOnce(scenario);
});

职责分层

  • Page Object:模拟真实用户操作。
  • API Client:读取稳定的业务状态。
  • Probe:只读观察数据库、缓存和事件。
  • Consistency Assertion:表达跨系统业务规则。
  • 测试文件:编排场景,不堆实现细节。
📝

失败报告与定位

说明断在哪一层

报告必须包含

  • e2e_run_id、订单号、traceId 和 eventId。
  • 用户操作、接口响应与执行环境。
  • 每个检查点的期望、实际状态和首次达到时间。
  • 数据库、缓存、消息和下游系统的状态时间线。
  • 最后一次轮询结果及超时阈值。
  • 相关日志、Trace、截图、消息重试和死信证据。
  • 初步归因:产品缺陷、环境故障、数据污染或脚本问题。

理想的失败结论

订单在 10:00:02 已取消,事件在 10:00:03 发布;优惠券消费者连续重试 3 次后进入死信队列。截至 10:05:02,优惠券状态仍为 USED。开发人员可以直接从消费者和死信记录开始排查。

端到端数据一致性检查清单

设计 / 执行 / 收尾

设计前

  • 已定义业务事实来源
  • 已区分强一致与最终一致
  • 已明确 SLA 和优先级
  • 已画出数据流向

执行中

  • 使用唯一关联标识
  • 正向与反向断言齐全
  • 异步状态使用轮询
  • 重复、超时、并发已覆盖

执行后

  • 失败证据可回放
  • 各层状态有时间线
  • 测试数据完成清理
  • 高风险用例进入持续回归