springboot的单元测试 vs 集成测试:为什么你每次 CI 红灯都想砸电脑?👍
写给每一个被「Redis 连不上」「动态数据源找不到主角」「Nacos 又来抢戏」支配恐惧的 Spring Boot 开发者
你是否经历过这些崩溃瞬间:
- 本地跑得飞起,一推 CI 就红成猴屁股
- 写个集成测试,启动 3 分钟才报错:
RedisConnectionFactory没了??? - Testcontainers 好不容易调通,结果 Windows 防火墙/网络把端口干掉了
@SpringBootTest一启动,全家桶(Nacos、Security、Zipkin、Cache、Actuator……)集体来访
如果以上任一条戳中你,那这篇文章可能是你今天最该读的。
单元测试和集成测试不是“谁更高级”,而是什么时候用哪把火。用错了,就是给自己挖坑;用对了,开发、提测、上线都能丝滑不少。
一、两位选手真实人设对比
| 项目 | 单元测试(小甜妹) | 集成测试(霸总) |
|---|---|---|
| 启动速度 | 毫秒级,眨眼就完 | 3–60 秒起步,爱你就要等得起 |
| 稳定性 | 超级稳,Mock 挡一切风雨 | 超级脆,一根网线就能翻车 |
| 问题定位 | 基本指哪打哪 | “牵一发动全身”,像破案 |
| 真实度 | 模拟约会,甜得虚假 | 直接领证,酸甜苦辣全都有 |
| 写/改爽度 | ★★★★★ | ★★☆☆☆(绿灯时爽到飞起) |
| 最推荐场景 | 核心逻辑、边界、控制器契约 | 数据库+缓存+MQ真实交互、跨模块契约 |
一句话总结:
单元测“我行不行”,集成测“我们行不行”。
二、最常踩的死亡坑(按翻车频率排序)
@SpringBootTest把全家桶都叫来了(Nacos、Security、Zipkin、Cache、Actuator……)RedisConnectionFactory找不到 /dynamic-datasource can not find primary datasource- Testcontainers 容器启动了,应用却连不上(写死 6379、Docker 网络冲突、镜像版本错配)
- 动态数据源 + 健康检查双重暴击
- 本地绿灯,CI 红灯(Docker 不可用、环境变量没传、端口冲突)
80% 的痛苦来源:范围没控制好 + 属性配置七零八落。
三、“三层火候”策略
别再一口吃成胖子,按这个梯度推进:
微火 · 单元测试(最先写,性价比最高)
- 控制器路由 + 参数绑定 + 返回结构
- Service 纯逻辑 + 边界条件
- DTO 映射、异常转换
- 推荐姿势:
MockMvc standaloneSetup或直接Mockito + new Controller()
中火 · 轻集成(性价比之王)
- 过滤器、拦截器、全局异常处理
- 消息序列化/反序列化
- 单数据层验证(
@DataJpaTest/@DataMongoTest) - 推荐姿势:
@WebMvcTest+ Mock Service/Repository
大火 · 重集成(谨慎使用,严格控范围)
- 真实数据库 + 缓存 + MQ 交互
- 跨模块/外部依赖契约验证
- 推荐姿势:
@SpringBootTest+Testcontainers+ 统一的@DynamicPropertySource
铁律:能 Mock 不连,能轻测不重,能拆开就不整锅端。
四、直接抄的救命代码模板
1. 控制器纯单元(最快最稳)
CharacterProfileService svc = mock(CharacterProfileService.class);
when(svc.saveProfile(anyString(), any())).thenReturn(1);
var controller = new CharacterProfileController(svc);
var mvc = MockMvcBuilders.standaloneSetup(controller).build();
mvc.perform(post("/api/characters/{id}/profile", "char-001")
.contentType(APPLICATION_JSON)
.content("""{"tenantId":"1"}"""))
.andExpect(status().isOk())
.andExpect(content().string("1"));
2. 多容器 Testcontainers 最小可用模板(2025 推荐)
Java
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class ProfileApiIntegrationTest {
@LocalServerPort int port;
@Container static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.36").withDatabaseName("test");
@Container static MongoDBContainer mongo = new MongoDBContainer("mongo:6.0");
@Container static GenericContainer<?> redis = new GenericContainer<>("redis:7.2").withExposedPorts(6379);
@DynamicPropertySource
static void props(DynamicPropertyRegistry reg) {
reg.add("spring.datasource.url", mysql::getJdbcUrl);
reg.add("spring.datasource.username", mysql::getUsername);
reg.add("spring.datasource.password", mysql::getPassword);
reg.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl);
reg.add("spring.data.redis.host", redis::getHost);
reg.add("spring.data.redis.port", redis::getFirstMappedPort);
}
// ... 你的测试方法
}
3. 常见冲突一键缓解(加到 @SpringBootTest)
Java
@SpringBootTest(properties = {
"management.health.db.enabled=false",
"spring.cloud.nacos.config.enabled=false",
"spring.cloud.nacos.discovery.enabled=false",
"spring.cache.type=none"
})
4.pom配置:依赖与插件(建议)
<dependencies>
<!-- Testcontainers 基础与常用模块(按需选择) -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 单元测试(Surefire)环境变量传递:用于远程 Docker 等场景 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<environmentVariables>
<DOCKER_HOST>${env.DOCKER_HOST}</DOCKER_HOST>
<DOCKER_TLS_VERIFY>${env.DOCKER_TLS_VERIFY}</DOCKER_TLS_VERIFY>
<DOCKER_CERT_PATH>${env.DOCKER_CERT_PATH}</DOCKER_CERT_PATH>
<TESTCONTAINERS_HOST_OVERRIDE>${env.TESTCONTAINERS_HOST_OVERRIDE}</TESTCONTAINERS_HOST_OVERRIDE>
<TESTCONTAINERS_RYUK_DISABLED>false</TESTCONTAINERS_RYUK_DISABLED>
</environmentVariables>
</configuration>
</plugin>
<!-- 集成测试(Failsafe)环境变量传递:用于 @IT 测试套件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<environmentVariables>
<DOCKER_HOST>${env.DOCKER_HOST}</DOCKER_HOST>
<DOCKER_TLS_VERIFY>${env.DOCKER_TLS_VERIFY}</DOCKER_TLS_VERIFY>
<DOCKER_CERT_PATH>${env.DOCKER_CERT_PATH}</DOCKER_CERT_PATH>
<TESTCONTAINERS_HOST_OVERRIDE>${env.TESTCONTAINERS_HOST_OVERRIDE}</TESTCONTAINERS_HOST_OVERRIDE>
<TESTCONTAINERS_RYUK_DISABLED>false</TESTCONTAINERS_RYUK_DISABLED>
</environmentVariables>
</configuration>
</plugin>
</plugins>
<!-- 注意:企业内网 Maven 仓库需使用 HTTPS,否则新版本 Maven 可能阻断 HTTP 仓库导致插件解析失败 -->
</build>
五、开测前必贴墙上的 10 条军规
- 先写单元,把契约定死,再逐步加集成
- 属性必须统一走 @DynamicPropertySource
- 能用 @WebMvcTest / @DataJpaTest 就别上 @SpringBootTest
- 端口永远用 getFirstMappedPort() 或 getMappedPort(xxx)
- 镜像版本要和生产驱动对齐(mysql 8.0、mongo 6、redis 7 常见组合)
- 断言别只写 isOk(),要验关键字段和结构
- 每个用例必须独立,严禁跨用例共享状态
- 日志开 debug,看 ConditionEvaluationReport 谁在乱入
- CI 环境变量要透传(DOCKER_HOST、TESTCONTAINERS_* 等)
- 本地绿了 ≠ 没问题,先跑 mvn clean verify -Ddocker.skip=false
最后想对你说❤️
测试不是为了“让 CI 变绿”,而是为了让你敢改代码、敢重构、敢上线。
把单元测试当成你的情绪护城河,把集成测试当成生产前的最后一道闸门。火候调对了,测试也能从精神内耗变成开发加速器。
愿你下一次 push,所有灯都是绿的。
也欢迎在评论区/朋友圈/群里贴出你最近踩的最离谱的坑,我们一起反杀~
(完)