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

微服务拆分精讲 / 第 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 MonkeyNetflix随机杀死实例
LitmusCNCFK8s 原生混沌工程
Chaos MeshPingCAPK8s 混沌平台
Gremlin商业企业级混沌平台
AWS FISAWSAWS 原生故障注入

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 流水线 — 独立部署、蓝绿发布、金丝雀发布。