第 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);
}
⚠️ 注意事项
- 不要过度依赖 E2E 测试——E2E 测试慢、脆弱、维护成本高
- 契约测试是关键——消费者驱动契约能预防 80% 的接口兼容性问题
- 混沌工程要谨慎——先在预发布环境实验,确认安全后再在生产环境执行
- 测试数据管理——使用工厂模式或 Fixture 管理测试数据
- 测试环境一致性——使用容器化保证测试环境与生产一致
📖 扩展阅读
- Pact Documentation (pact.io) — 契约测试框架
- Testcontainers (testcontainers.org) — 容器化集成测试
- Chaos Mesh (chaos-mesh.org) — K8s 混沌工程平台
- k6 Documentation (k6.io) — 现代化性能测试工具
- Testing Microservices — Sam Newman — 微服务测试策略
本章小结
| 测试类型 | 作用 | 频率 | 工具 |
|---|
| 单元测试 | 验证业务逻辑 | 每次提交 | JUnit, Mockito |
| 契约测试 | 验证接口兼容性 | 每次提交 | Pact |
| 集成测试 | 验证组件协作 | 每天 | Testcontainers |
| E2E 测试 | 验证关键路径 | 每次发布 | Selenium, Cypress |
| 混沌工程 | 验证系统弹性 | 定期 | Chaos Mesh, Litmus |
| 性能测试 | 验证性能指标 | 每周/每月 | k6, Gatling |
📌 下一章:第 13 章:CI/CD 流水线 — 独立部署、蓝绿发布、金丝雀发布。