返回分布式与数据质量模块
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.count
09

完成一次微服务故障演练

练习与验收

练习:验证商城交易链

  1. 画出网关、订单、库存、优惠、支付和退款依赖图。
  2. 为未认证、越权和重复幂等键写“当……时,……”用例。
  3. 建立订单消费者与库存提供者契约。
  4. 注入库存超时,确认超时预算和重试次数。
  5. 让优惠服务持续失败,观察熔断、降级和恢复。
  6. 在 Saga 每个步骤后中断,核对补偿与最终状态。
  7. 验证支付成功但确认超时时不会重复扣款。
  8. 用 traceId 和订单号还原一次失败时间线。

边界清楚

  • 依赖地图完整
  • 契约版本化
  • 认证授权覆盖
  • 幂等键可追踪

故障可控

  • 超时预算合理
  • 重试不放大
  • 熔断可恢复
  • 降级不误导

状态可信

  • 补偿可重放
  • 最终状态可验证
  • 追踪链完整
  • 敏感信息脱敏

同步调用和分布式事务已经可测。下一步进入消息重复、丢失、乱序和积压的异步世界。

继续学习消息队列测试