强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

微服务拆分精讲 / 第 12 章:测试策略

第 12 章:测试策略

微服务的测试金字塔不再是简单的三层结构。契约测试和混沌工程是新增的关键层。


12.1 微服务测试的挑战

12.1.1 单体 vs 微服务测试对比

  单体测试:                     微服务测试:

  ┌──────────────────┐          ┌────┐ ┌────┐ ┌────┐
  │  集成测试         │          │ 单元│ │ 单元│ │ 单元│
  │  (一个应用内)    │          └────┘ └────┘ └────┘
  │  ✅ 简单          │             │      │      │
  │  ✅ 快速          │          ┌────────────────────┐
  │  ✅ 环境一致      │          │  契约测试 (Contract) │
  │                   │          │  ✅ 验证接口兼容性   │
  └──────────────────┘          └────────────────────┘
                                      │
                                  ┌────────────────────┐
                                  │  集成测试            │
                                  │  ⚠️ 需要多服务环境   │
                                  └────────────────────┘
                                      │
                                  ┌────────────────────┐
                                  │  端到端测试          │
                                  │  ❌ 复杂、慢、脆弱   │
                                  └────────────────────┘

12.1.2 核心挑战

挑战 说明
环境依赖 测试需要多个服务同时运行
数据准备 跨服务的测试数据难以管理
接口变更 上游服务变更可能破坏下游
测试速度 多服务环境启动慢
不确定性 网络、超时等分布式因素引入不确定性

12.2 测试金字塔(微服务版)

                    ┌─────────┐
                    │  E2E    │  少量
                    │ 端到端   │  (每次发布)
                    ├─────────┤
                    │集成测试  │  适量
                    │         │  (每天)
                   ┌┴─────────┴┐
                   │  契约测试   │  较多
                   │ (Contract) │  (每次提交)
                  ┌┴─────────────┴┐
                  │    单元测试     │  大量
                  │   (Unit Test)  │  (每次提交)
                  └────────────────┘

  测试数量:单元 > 契约 > 集成 > E2E
  测试速度:单元 > 契约 > 集成 > E2E
  测试成本:单元 < 契约 < 集成 < E2E

12.3 单元测试

12.3.1 微服务单元测试范围

// 订单服务的单元测试示例
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private InventoryClient inventoryClient;

    @Mock
    private PaymentClient paymentClient;

    @InjectMocks
    private OrderService orderService;

    @Test
    @DisplayName("创建订单 - 库存充足且支付成功")
    void createOrder_Success() {
        // Given
        CreateOrderCommand command = new CreateOrderCommand("user-1",
            List.of(new OrderItem("prod-1", 2, new Money(100, "CNY"))));

        when(inventoryClient.checkStock("prod-1", 2)).thenReturn(true);
        when(orderRepository.save(any(Order.class)))
            .thenAnswer(inv -> inv.getArgument(0));
        when(paymentClient.createPayment(any())).thenReturn(new PaymentResult("PAY-1", "SUCCESS"));

        // When
        Order order = orderService.createOrder(command);

        // Then
        assertNotNull(order.getOrderId());
        assertEquals(OrderStatus.CREATED, order.getStatus());
        verify(inventoryClient).deductStock("prod-1", 2);
        verify(paymentClient).createPayment(any());
    }

    @Test
    @DisplayName("创建订单 - 库存不足应抛出异常")
    void createOrder_InsufficientStock_ShouldThrow() {
        // Given
        when(inventoryClient.checkStock("prod-1", 2)).thenReturn(false);

        // When & Then
        assertThrows(InsufficientStockException.class,
            () -> orderService.createOrder(command));
    }
}

12.3.2 单元测试最佳实践

实践 说明
Mock 外部依赖 使用 Mock/Stub 隔离外部服务
测试业务逻辑 重点测试领域逻辑,不测试框架代码
测试边界条件 空值、负数、超大值、并发
测试命名清晰 方法名_场景_期望结果
测试独立性 每个测试独立运行,不依赖其他测试

12.4 契约测试(Contract Testing)

12.4.1 为什么需要契约测试

  问题场景:

  订单服务 (Consumer)              商品服务 (Provider)
  ┌──────────────┐                ┌──────────────┐
  │ 期望响应:     │                │ 实际响应:     │
  │ {             │                │ {             │
  │  "price": 100 │  ══ 不匹配 ══  │  "unit_price" │  ← 字段名改了!
  │ }             │                │  : 100        │
  │               │                │ }             │
  └──────────────┘                └──────────────┘

  契约测试的目的:
  → 在部署前发现这种接口不兼容的问题

12.4.2 Pact 契约测试

Pact 是最流行的消费者驱动契约测试(Consumer-Driven Contract Testing)框架。

  Pact 工作流程:

  ┌──────────────┐                    ┌──────────────┐
  │ 消费者测试    │                    │ 提供者验证    │
  │ (Consumer)   │                    │ (Provider)   │
  ├──────────────┤                    ├──────────────┤
  │              │                    │              │
  │ 1. 定义期望   │                    │ 3. 获取契约   │
  │    的交互     │                    │              │
  │              │                    │ 4. 用真实服务  │
  │ 2. 生成契约   │───────────────────▶│    验证交互   │
  │    (Pact文件) │  Pact Broker       │              │
  │              │◀───────────────────│ 5. 验证通过   │
  └──────────────┘                    └──────────────┘

12.4.3 Pact 消费者测试示例

// 订单服务(消费者)测试商品服务接口
@Pact(consumer = "order-service", provider = "product-service")
public RequestResponsePact getProductPact(PactDslWithProvider builder) {
    return builder
        .given("product PROD-001 exists")
        .uponReceiving("get product by id")
        .path("/api/v1/products/PROD-001")
        .method("GET")
        .willRespondWith()
        .status(200)
        .body(new PactDslJsonBody()
            .stringType("id", "PROD-001")
            .stringType("name", "iPhone 15")
            .decimalType("price", 5999.00)
            .integerType("stock", 100))
        .toPact();
}

@PactTestFor(pactMethod = "getProductPact")
@Test
void testGetProduct() {
    // 调用真实的消费者代码,但 Mock 的提供者响应
    Product product = productClient.getProduct("PROD-001");

    assertEquals("PROD-001", product.getId());
    assertEquals("iPhone 15", product.getName());
    assertEquals(new BigDecimal("5999.00"), product.getPrice());
}

12.4.4 Pact 提供者验证

// 商品服务(提供者)验证
@Provider("product-service")
@PactFolder("pacts")  // 或从 Pact Broker 获取
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductServiceProviderTest {

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void verifyPact(Pact pact, Interaction interaction, HttpRequest request,
                    PactVerificationContext context) {
        context.verifyInteraction();
    }

    @State("product PROD-001 exists")
    void setupProductExists() {
        // 准备测试数据
        productRepository.save(new Product("PROD-001", "iPhone 15",
            new BigDecimal("5999.00"), 100));
    }
}

12.5 集成测试

12.5.1 集成测试策略

  集成测试类型:

  1. 服务内集成测试(测试服务与数据库的交互)
     ┌──────────┐     ┌──────────┐
     │ 服务代码  │────▶│  Test    │
     │          │     │  DB      │  (Testcontainers)
     └──────────┘     └──────────┘

  2. 服务间集成测试(测试服务间的真实调用)
     ┌──────────┐     ┌──────────┐     ┌──────────┐
     │ 服务 A   │────▶│  服务 B  │────▶│  真实 DB  │
     │          │     │          │     │          │
     └──────────┘     └──────────┘     └──────────┘
     (在 Docker Compose 环境中测试)

  3. 组件集成测试(测试消息队列、缓存等中间件)
     ┌──────────┐     ┌──────────┐     ┌──────────┐
     │  服务     │────▶│  Kafka   │────▶│  消费者   │
     │          │     │ (Docker) │     │          │
     └──────────┘     └──────────┘     └──────────┘

12.5.2 Testcontainers

@Testcontainers
class OrderRepositoryIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("order_db")
        .withUsername("test")
        .withPassword("test");

    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldSaveAndRetrieveOrder() {
        Order order = new Order("ORD-001", "USER-001", OrderStatus.CREATED);
        orderRepository.save(order);

        Order found = orderRepository.findById("ORD-001").orElseThrow();
        assertEquals("ORD-001", found.getOrderId());
        assertEquals(OrderStatus.CREATED, found.getStatus());
    }
}

12.5.3 Docker Compose 测试环境

# docker-compose.test.yml
version: '3.8'
services:
  user-service:
    image: user-service:latest
    ports: ["8081:8080"]
    environment:
      SPRING_PROFILES_ACTIVE: test
    depends_on: [user-db]

  order-service:
    image: order-service:latest
    ports: ["8082:8080"]
    environment:
      SPRING_PROFILES_ACTIVE: test
      USER_SERVICE_URL: http://user-service:8080
    depends_on: [order-db, kafka]

  user-db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: user_db
      MYSQL_ROOT_PASSWORD: test

  order-db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: order_db
      MYSQL_ROOT_PASSWORD: test

  kafka:
    image: confluentinc/cp-kafka:7.5.0
    ports: ["9092:9092"]

12.6 端到端测试(E2E)

12.6.1 E2E 测试策略

  E2E 测试的"冰淇淋反模式" vs 正确做法:

  ❌ 冰淇淋反模式(太多 E2E)
     ┌────────────────────┐
     │    大量 E2E 测试    │  慢、脆弱、难维护
     │                    │
     ├────────────────────┤
     │  少量单元测试       │
     └────────────────────┘

  ✅ 测试金字塔(正确的比例)
     ┌──────┐
     │ E2E  │  少量关键路径
     ├──────┤
     │集成  │  适量
     ├──────┤
     │契约  │  较多
     ├──────┤
     │单元  │  大量
     └──────┘

12.6.2 E2E 测试范围

测试场景 是否需要 E2E 理由
核心下单流程 ✅ 必须 最重要的业务路径
用户注册登录 ✅ 必须 安全关键路径
支付流程 ✅ 必须 资金关键路径
边界条件 ❌ 不需要 用单元测试覆盖
错误处理 ❌ 不需要 用集成测试覆盖

12.7 混沌工程(Chaos Engineering)

12.7.1 什么是混沌工程

混沌工程通过在系统中注入故障,验证系统的弹性和容错能力。

  混沌工程实验流程:

  1. 定义稳态假设
     ────────────────
     "系统的 99th 百分位延迟 < 500ms"

  2. 注入故障
     ────────────────
     • 杀死一个服务实例
     • 注入网络延迟 (500ms)
     • 磁盘填满
     • CPU 打满

  3. 观察系统行为
     ────────────────
     • P99 延迟是否超过 500ms?
     • 熔断器是否触发?
     • 服务是否自动恢复?
     • 告警是否正常触发?

  4. 分析结果
     ────────────────
     • 如果系统行为符合预期 → ✅ 实验成功
     • 如果系统行为异常 → ❌ 修复后重试

12.7.2 Chaos Engineering 工具

工具 开发者 特点
Chaos Monkey Netflix 随机杀死实例
Litmus CNCF K8s 原生混沌工程
Chaos Mesh PingCAP K8s 混沌平台
Gremlin 商业 企业级混沌平台
AWS FIS AWS AWS 原生故障注入

12.7.3 Chaos Mesh 实验示例

# 注入 Pod 故障:随机杀死订单服务的 Pod
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
  name: order-service-kill
  namespace: production
spec:
  action: pod-kill
  mode: one
  selector:
    labelSelectors:
      app: order-service
  scheduler:
    cron: '@every 30m'  # 每 30 分钟杀死一个 Pod
# 注入网络延迟
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: payment-service-delay
spec:
  action: delay
  mode: all
  selector:
    labelSelectors:
      app: payment-service
  delay:
    latency: "200ms"
    jitter: "50ms"
    correlation: "50"
  duration: "5m"

12.7.4 混沌实验检查清单

  推荐的混沌实验:

  □ 杀死服务实例 → 验证 K8s 自动重启
  □ 注入网络延迟 → 验证超时和熔断
  □ 注入网络分区 → 验证服务降级
  □ 填满磁盘 → 验证日志轮转和告警
  □ 打满 CPU → 验证自动扩容
  □ 杀死数据库主节点 → 验证主从切换
  □ 关闭消息队列 → 验证消息重试和补偿
  □ DNS 故障 → 验证服务发现容错

12.8 性能测试

12.8.1 性能测试类型

类型 目的 工具
负载测试 验证正常负载下的性能 JMeter, Gatling, k6
压力测试 找到系统的性能瓶颈 JMeter, Gatling
浸泡测试 长时间运行检测内存泄漏 JMeter
峰值测试 验证突发流量的处理能力 k6, Locust

12.8.2 k6 性能测试示例

// order-performance-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // 升压到 100 VU
    { duration: '5m', target: 100 },  // 保持 100 VU
    { duration: '2m', target: 200 },  // 升压到 200 VU
    { duration: '5m', target: 200 },  // 保持 200 VU
    { duration: '2m', target: 0 },    // 降压
  ],
  thresholds: {
    http_req_duration: ['p(99)<500'],  // P99 < 500ms
    http_req_failed: ['rate<0.01'],    // 错误率 < 1%
  },
};

export default function () {
  const payload = JSON.stringify({
    userId: `user-${__VU}`,
    items: [{ productId: 'PROD-001', quantity: 1 }]
  });

  const params = { headers: { 'Content-Type': 'application/json' } };
  const res = http.post('http://api-gateway/api/v1/orders', payload, params);

  check(res, {
    'status is 201': (r) => r.status === 201,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

⚠️ 注意事项

  1. 不要过度依赖 E2E 测试——E2E 测试慢、脆弱、维护成本高
  2. 契约测试是关键——消费者驱动契约能预防 80% 的接口兼容性问题
  3. 混沌工程要谨慎——先在预发布环境实验,确认安全后再在生产环境执行
  4. 测试数据管理——使用工厂模式或 Fixture 管理测试数据
  5. 测试环境一致性——使用容器化保证测试环境与生产一致

📖 扩展阅读

  1. Pact Documentation (pact.io) — 契约测试框架
  2. Testcontainers (testcontainers.org) — 容器化集成测试
  3. Chaos Mesh (chaos-mesh.org) — K8s 混沌工程平台
  4. k6 Documentation (k6.io) — 现代化性能测试工具
  5. Testing Microservices — Sam Newman — 微服务测试策略

本章小结

测试类型 作用 频率 工具
单元测试 验证业务逻辑 每次提交 JUnit, Mockito
契约测试 验证接口兼容性 每次提交 Pact
集成测试 验证组件协作 每天 Testcontainers
E2E 测试 验证关键路径 每次发布 Selenium, Cypress
混沌工程 验证系统弹性 定期 Chaos Mesh, Litmus
性能测试 验证性能指标 每周/每月 k6, Gatling

📌 下一章第 13 章:CI/CD 流水线 — 独立部署、蓝绿发布、金丝雀发布。