返回分布式与数据质量模块一次下单穿过的核心服务
消费者驱动契约反馈环
熔断器不是一个错误码
Distributed Systems / Tutorial 16
微服务测试实战教程
不只验证每个服务“各自正常”,还要验证依赖失败、网络不确定和跨服务状态变化时,交易链路仍可控。
9 个章节订单服务群契约 + 韧性 + 追踪
01
先画出订单链路与责任边界
知道失败会传到哪里网关路由与认证
订单编排交易
库存预占与释放
优惠计算与核销
支付扣款与退款
建立服务测试地图
- 记录服务负责人、接口契约、存储和上下游。
- 标出同步调用、异步事件与第三方边界。
- 为每条调用记录超时、重试、幂等和降级策略。
- 列出订单号、支付号、traceId 等关联键。
成功响应统一检查 HTTP 200,但微服务的业务结果仍由响应体业务码和跨服务最终状态决定。
02
把风险放到合适的测试层
不要全靠 E2E微服务测试的四个层级
| 层级 | 验证目标 | 优势 | 限制 |
|---|---|---|---|
| 单服务 | 订单金额、状态规则 | 快,原因明确 | 不能证明真实依赖 |
| 契约 | 请求响应结构与兼容 | 提前发现接口破坏 | 不能证明业务协同 |
| 组件 | 服务 + 数据库 + 可控依赖 | 可验证存储与故障 | 环境仍被简化 |
| 端到端 | 网关到库存、支付 | 接近真实链路 | 慢且定位成本高 |
当优惠规则改变时,怎样分层
- 单元测试穷举门槛、叠加与金额精度。
- 契约测试保证订单服务仍能解析优惠响应。
- 组件测试验证优惠超时和数据库写入。
- 只保留少量 E2E 验证真实下单和取消。
03
验证网关路由、认证与授权
入口安全边界当……时,……网关用例
| 场景 | 预期 |
|---|---|
| 当访问令牌有效且用户拥有订单时,查询详情 | HTTP 200,响应体返回自己的订单 |
| 当令牌过期时,提交订单 | 401,网关不转发到订单服务 |
| 当普通用户查询他人订单时 | 403 或业务拒绝,响应体不泄露订单信息 |
| 当重复提交相同幂等键时 | HTTP 200,响应体指向同一订单 |
pytest 网关验证
def test_当用户查询他人订单时_网关拒绝(gateway, user_token):
response = gateway.get("/api/orders/OTHER-1", token=user_token)
assert response.status_code == 403
assert "paidAmount" not in response.text
def test_当重复提交相同幂等键时_只创建一个订单(gateway, payload):
headers = {"Idempotency-Key": "case-order-001"}
first = gateway.post("/api/orders", json=payload, headers=headers)
second = gateway.post("/api/orders", json=payload, headers=headers)
assert first.status_code == second.status_code == 200
assert first.json()["data"]["orderId"] == second.json()["data"]["orderId"]04
用契约测试控制服务演进
兼容消费者订单消费者声明期望
契约仓库版本化保存
库存提供者验证实现
部署门禁阻断破坏
库存响应契约测试
def test_inventory_reserve_contract(inventory_client):
response = inventory_client.reserve({
"orderId": "O-CONTRACT-1", "skuId": "SKU-1", "quantity": 1
})
assert response.status_code == 200
body = response.json()
assert body["businessCode"] == "SUCCESS"
assert isinstance(body["data"]["reservationId"], str)
assert body["data"]["status"] == "RESERVED"新增可选字段通常兼容,删除字段、改名、改类型或改变业务语义可能破坏消费者。契约通过后仍需保留真实集成测试。
05
验证超时、重试与幂等共同工作
重试会放大副作用同步依赖故障目录
| 故障 | 系统行为 | 重点断言 |
|---|---|---|
| 库存超时 | 订单不应无限等待 | 超时预算、无重复预占 |
| 优惠 500 | 按策略失败或降级 | 金额提示明确 |
| 支付慢响应 | 客户端不可重复扣款 | 幂等键、状态可查询 |
| 退款服务不可用 | 进入可恢复状态 | 任务可重试、用户可查询 |
toxiproxy 故障注入示例
# 给支付上游增加 1500ms 延迟
curl -X POST http://localhost:8474/proxies/payment/toxics \
-H "Content-Type: application/json" \
-d '{"name":"latency","type":"latency","attributes":{"latency":1500}}'
# 用同一幂等键发起支付;成功时 HTTP 200,仍检查响应体
curl -X POST http://localhost:8080/api/payments \
-H "Idempotency-Key: pay-O-1001" \
-d '{"orderId":"O-1001","amount":"80.00"}'时间预算
入口总超时必须大于内部关键步骤之和,并为序列化、排队和网络留出余量。下游超时不能比上游等待更长,否则请求已经放弃,下游仍继续消耗资源。
06
测试熔断、降级、限流和恢复
状态转换可观察Closed正常调用
失败超阈值记录并打开
Open快速失败或降级
Half-open少量探测
恢复重新关闭
故障演练断言
def test_当优惠服务持续超时时_熔断并安全降级(shop):
shop.inject_timeout(service="coupon", count=10)
results = [shop.preview_order("SKU-1") for _ in range(12)]
assert any(r.headers.get("X-Circuit-State") == "OPEN" for r in results)
assert all(r.status_code == 200 for r in results)
assert all(r.json()["businessCode"] == "COUPON_UNAVAILABLE" for r in results)
assert shop.count_created_orders() == 0降级必须对用户透明说明风险。无法确认优惠金额时,不应悄悄按原价创建订单;恢复后还要验证熔断器不会长期停留在半开状态。
07
验证分布式事务与补偿
最终一致但不可失控Saga 中断点与补偿
| 中断点 | 补偿动作 | 最终断言 |
|---|---|---|
| 创建订单后锁库存失败 | 订单取消 | 没有库存预占 |
| 锁库存后支付失败 | 释放库存 | 订单保持待支付或关闭 |
| 支付成功但确认超时 | 先查询再补偿 | 不得再次扣款 |
| 退款成功但订单更新失败 | 补偿任务重放 | 资金与订单最终一致 |
跨服务状态轮询
def wait_until(fetch, predicate, timeout=10):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
value = fetch()
if predicate(value):
return value
time.sleep(0.2)
raise AssertionError("business state did not converge")
def test_当支付失败时_库存最终释放(order_api, inventory_api, order):
order_api.pay(order["orderId"], force="DECLINED")
reservation = wait_until(
lambda: inventory_api.get(order["reservationId"]),
lambda x: x["status"] == "RELEASED",
)
assert reservation["releasedQuantity"] == 1不要用固定等待猜测最终一致。轮询业务状态,并设置明确超时;同时检查补偿只执行一次、重复触发幂等、人工修复入口可用。
08
用日志、指标与追踪还原故障传播
跨服务证据链一条 trace 应回答
- 请求经过哪些服务,各阶段耗时多少。
- traceId、orderId、paymentId 是否贯穿且可搜索。
- 哪次调用超时、重试、熔断或进入补偿。
- 响应中的错误是否映射到正确责任服务。
- 日志不泄露令牌、卡号、地址等敏感信息。
W3C Trace Context 调用示例
curl -i http://localhost:8080/api/orders/O-1001 \
-H "Authorization: Bearer $TEST_TOKEN" \
-H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
# 检查:网关、订单、库存、支付 span 共享同一 traceId
# 检查:错误 span 带 order.id、peer.service 与 retry.count09
完成一次微服务故障演练
练习与验收练习:验证商城交易链
- 画出网关、订单、库存、优惠、支付和退款依赖图。
- 为未认证、越权和重复幂等键写“当……时,……”用例。
- 建立订单消费者与库存提供者契约。
- 注入库存超时,确认超时预算和重试次数。
- 让优惠服务持续失败,观察熔断、降级和恢复。
- 在 Saga 每个步骤后中断,核对补偿与最终状态。
- 验证支付成功但确认超时时不会重复扣款。
- 用 traceId 和订单号还原一次失败时间线。
边界清楚
- 依赖地图完整
- 契约版本化
- 认证授权覆盖
- 幂等键可追踪
故障可控
- 超时预算合理
- 重试不放大
- 熔断可恢复
- 降级不误导
状态可信
- 补偿可重放
- 最终状态可验证
- 追踪链完整
- 敏感信息脱敏
同步调用和分布式事务已经可测。下一步进入消息重复、丢失、乱序和积压的异步世界。
继续学习消息队列测试