👍springboot的单元测试 vs 集成测试:为什么你每次 CI 红灯都想砸电脑?

12 阅读4分钟

springboot的单元测试 vs 集成测试:为什么你每次 CI 红灯都想砸电脑?👍

写给每一个被「Redis 连不上」「动态数据源找不到主角」「Nacos 又来抢戏」支配恐惧的 Spring Boot 开发者

你是否经历过这些崩溃瞬间:

  • 本地跑得飞起,一推 CI 就红成猴屁股
  • 写个集成测试,启动 3 分钟才报错:RedisConnectionFactory 没了???
  • Testcontainers 好不容易调通,结果 Windows 防火墙/网络把端口干掉了
  • @SpringBootTest 一启动,全家桶(Nacos、Security、Zipkin、Cache、Actuator……)集体来访

如果以上任一条戳中你,那这篇文章可能是你今天最该读的。

单元测试和集成测试不是“谁更高级”,而是什么时候用哪把火。用错了,就是给自己挖坑;用对了,开发、提测、上线都能丝滑不少。

一、两位选手真实人设对比

项目单元测试(小甜妹)集成测试(霸总)
启动速度毫秒级,眨眼就完3–60 秒起步,爱你就要等得起
稳定性超级稳,Mock 挡一切风雨超级脆,一根网线就能翻车
问题定位基本指哪打哪“牵一发动全身”,像破案
真实度模拟约会,甜得虚假直接领证,酸甜苦辣全都有
写/改爽度★★★★★★★☆☆☆(绿灯时爽到飞起)
最推荐场景核心逻辑、边界、控制器契约数据库+缓存+MQ真实交互、跨模块契约

一句话总结:
单元测“我行不行”,集成测“我们行不行”

二、最常踩的死亡坑(按翻车频率排序)

  1. @SpringBootTest 把全家桶都叫来了(Nacos、Security、Zipkin、Cache、Actuator……)
  2. RedisConnectionFactory 找不到 / dynamic-datasource can not find primary datasource
  3. Testcontainers 容器启动了,应用却连不上(写死 6379、Docker 网络冲突、镜像版本错配)
  4. 动态数据源 + 健康检查双重暴击
  5. 本地绿灯,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 条军规

  1. 先写单元,把契约定死,再逐步加集成
  2. 属性必须统一走 @DynamicPropertySource
  3. 能用 @WebMvcTest / @DataJpaTest 就别上 @SpringBootTest
  4. 端口永远用 getFirstMappedPort() 或 getMappedPort(xxx)
  5. 镜像版本要和生产驱动对齐(mysql 8.0、mongo 6、redis 7 常见组合)
  6. 断言别只写 isOk(),要验关键字段和结构
  7. 每个用例必须独立,严禁跨用例共享状态
  8. 日志开 debug,看 ConditionEvaluationReport 谁在乱入
  9. CI 环境变量要透传(DOCKER_HOST、TESTCONTAINERS_* 等)
  10. 本地绿了 ≠ 没问题,先跑 mvn clean verify -Ddocker.skip=false

最后想对你说❤️

测试不是为了“让 CI 变绿”,而是为了让你敢改代码、敢重构、敢上线

把单元测试当成你的情绪护城河,把集成测试当成生产前的最后一道闸门。火候调对了,测试也能从精神内耗变成开发加速器。

愿你下一次 push,所有灯都是绿的。

也欢迎在评论区/朋友圈/群里贴出你最近踩的最离谱的坑,我们一起反杀~

(完)