微服务集成测试策略——服务依赖 Mock vs 真实服务的取舍与实践
微服务集成测试策略——服务依赖 Mock vs 真实服务的取舍与实践
适读人群:微服务架构开发者、技术负责人 | 阅读时长:约 17 分钟 | 核心价值:掌握微服务集成测试的策略选择,在速度、真实性、维护成本之间找到最优平衡
做微服务架构的测试,最难的不是技术,是决策:哪些依赖要 Mock,哪些要用真实服务?
这个问题没有标准答案,但有原则。我见过两种极端,两种都很痛苦。
极端一:什么都 Mock A 团队把所有服务间依赖都 Mock 掉。表面上,每个服务的测试都能独立快速运行。但每次上线,总会有"单测通过、集成失败"的情况,因为 Mock 和真实服务之间的行为差异从来没有被验证过。
极端二:什么都用真实服务 B 团队坚持用真实的所有服务做集成测试。每次跑一个服务的测试,需要先部署一整套微服务环境,稳定性差、启动慢、维护成本高,测试最后成了摆设,没人愿意跑。
我们团队经历过 A,踩过 B 的坑,最后找到了一个中间路线。今天这篇,把这套策略完整写出来。
一、微服务依赖的分类
在做决策之前,先对依赖进行分类:
第一类:基础设施依赖(自己管理的中间件)
- MySQL、PostgreSQL、Redis、Kafka、Elasticsearch
- 推荐:Testcontainers 真实容器
- 原因:这类中间件的行为特性(事务、序列化、查询语法)直接影响业务逻辑
第二类:内部服务依赖(团队内其他微服务)
- 用户服务、商品服务、库存服务
- 推荐:Spring Cloud Contract Stub / Pact Mock
- 原因:内部服务可以维护契约,保证 Mock 行为与真实实现一致
第三类:外部 HTTP 服务(第三方 API)
- 支付宝、微信支付、短信服务商
- 推荐:WireMock
- 原因:第三方服务不在控制范围内,沙箱不稳定,用 WireMock 模拟各种响应场景
第四类:纯业务 Bean(本服务内部的 Service 层)
- 不建议在集成测试里 Mock
- 在集成测试层,要测的就是这些 Bean 的真实行为
二、分层测试策略
层次一:Component Test(组件测试)
范围:单个微服务,依赖都用测试替身(Stub/Mock/Testcontainers)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@AutoConfigureStubRunner(
ids = "com.example:user-service:+:stubs", // 用户服务 Stub
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class OrderServiceComponentTest {
// 真实的基础设施
@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.36");
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));
// 外部 HTTP 服务 Mock
@Container
static WireMockContainer paymentMock = new WireMockContainer("wiremock/wiremock:3.3.1")
.withMapping("payment-success",
WireMockContainer.WireMockMappingSource.classpath("wiremock/payment.json"));
@Test
void 创建订单_调用用户服务验证_库存扣减_Kafka发消息() {
// 这个测试验证的是:
// 1. 调用用户 Stub 获取用户信息(验证契约)
// 2. 扣减真实 MySQL 里的库存
// 3. 发送真实 Kafka 消息
// 4. 调用支付 WireMock 创建支付单
// given
Long orderId = createOrder(1001L, "PROD-100", 2);
// then
await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> {
// 验证 MySQL 里的订单记录
Order order = orderRepository.findById(orderId).orElseThrow();
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING_PAYMENT);
// 验证 Kafka 消息被发出
// ... Kafka 消费验证逻辑
});
}
}层次二:Integration Smoke Test(集成冒烟测试)
范围:核心路径,用 Docker Compose 拉起最小化的真实服务集合
# docker-compose.test.yml
version: '3.8'
services:
user-service:
image: user-service:latest
ports:
- "8081:8080"
environment:
- SPRING_PROFILES_ACTIVE=test
order-service:
image: order-service:latest
ports:
- "8082:8080"
depends_on:
- user-service
- mysql
- kafka
mysql:
image: mysql:8.0.36
environment:
MYSQL_ROOT_PASSWORD: test
kafka:
image: confluentinc/cp-kafka:7.5.0// 集成冒烟测试(只在特定场景跑,不是每次 CI 都跑)
@SpringBootTest
@Testcontainers
class OrderUserIntegrationSmokeTest {
@Container
static DockerComposeContainer<?> compose =
new DockerComposeContainer<>(new File("docker-compose.test.yml"))
.withExposedService("order-service", 8080,
Wait.forHttp("/actuator/health").forStatusCode(200));
@Test
@Tag("smoke") // 只在 smoke 测试 profile 下运行
void 创建订单_完整微服务链路_端到端通过() {
String orderServiceUrl = "http://" + compose.getServiceHost("order-service", 8080)
+ ":" + compose.getServicePort("order-service", 8080);
given()
.baseUri(orderServiceUrl)
.contentType(ContentType.JSON)
.body("""
{"userId": 1001, "productId": 100, "quantity": 1}
""")
.when()
.post("/api/orders")
.then()
.statusCode(201);
}
}三、决策矩阵
| 依赖类型 | 频繁变更 | 行为复杂 | 有外部接口契约 | 推荐方案 |
|---|---|---|---|---|
| 数据库 | 是 | 是 | N/A | Testcontainers |
| 消息队列 | 是 | 是 | N/A | Testcontainers |
| 内部服务(稳定) | 否 | 否 | 有契约 | Contract Stub |
| 内部服务(频繁变更) | 是 | 是 | 有契约 | Contract Stub + 周期性 E2E |
| 第三方 HTTP API | 否(受对方控制) | 是 | 否 | WireMock |
| 外部认证服务 | 否 | 是 | 有文档 | WireMock + 偶发真实测试 |
四、三个踩坑实录
坑 1:Stub 行为过于理想化
现象: 用 Spring Cloud Contract Stub 测通了,上线后和真实服务对接出了问题,服务偶发返回 500,但 Stub 永远返回 200。
原因: Stub 只模拟了"正常路径",没有模拟各种错误场景(超时、500、网络中断)。
解法: 在 Stub 里增加错误场景契约,同时在消费者测试里测试错误处理逻辑:
// 提供者契约里增加错误场景
Contract.make {
description "用户服务内部错误"
request {
method GET()
url '/api/users/trigger-error' // 特殊的 trigger 用户 ID
}
response {
status INTERNAL_SERVER_ERROR()
body([error: "Internal Server Error"])
}
}坑 2:Docker Compose 集成测试在 CI 太慢
现象: Docker Compose 方式的集成测试在 CI 上需要 15 分钟,严重拖慢了流水线。
原因: Docker Compose 需要启动多个服务,每个服务都需要 JVM 预热时间,加在一起很慢。
解法: Docker Compose 集成测试不放在常规 CI 里,只在 merge 到 main 时运行,或者每天定时运行:
# GitHub Actions
on:
schedule:
- cron: '0 2 * * *' # 每天凌晨 2 点
push:
branches:
- main # main 分支合并时运行
jobs:
integration-smoke:
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
# ... Docker Compose 测试日常 PR 的 CI 只跑 Component Test(组件测试,用 Testcontainers),速度快。
坑 3:服务注册与发现在测试环境的混乱
现象: 测试环境开启了 Consul/Eureka 服务注册,测试里启动的服务实例被注册到 Consul,影响了其他测试环境的服务发现。
原因: 测试环境的 Spring Boot 应用默认注册到服务注册中心,多个测试并发时,注册中心里有大量测试实例。
解法: 测试 profile 禁用服务注册:
# application-test.yml
spring:
cloud:
consul:
discovery:
register: false # 测试环境不注册到 Consul
eureka:
client:
register-with-eureka: false
fetch-registry: false五、策略选择的底线原则
不管选哪种策略,这三条底线不能突破:
基础设施不能 Mock:数据库、消息队列必须用真实容器。Mock 掩盖的都是最容易出生产 Bug 的地方。
Mock 的行为必须有合约约束:Mock 了其他服务,必须有机制保证 Mock 行为和真实服务一致(契约测试就是干这个的)。
至少有一套端到端的冒烟测试:即使只是核心链路,也要定期用真实服务跑一遍端到端。
Mock vs 真实,不是非此即彼,而是组合拳。用对地方,才能又快又准。
