背景#

在一次常规的集成测试构建中,发现集成测试整体耗时偏高,影响开发反馈速度。

项目技术栈是 Spring Boot + WebFlux + R2DBC + Reactive Redis + Kafka,测试侧大量使用 Testcontainers。


问题:集成测试为什么慢?#

观察结果#

  • 单测(Surefire)总耗时约:3.4s
  • 集成测试(Failsafe)总耗时约:270s

瓶颈明显在 IT 阶段。

关键原因#

  1. 测试类型混跑
    • 一个纯 Mockito 测试命名为 *IT.java,被 Failsafe 执行。
  2. 容器启动重复
    • BaseIntegrationTestBaseR2dbcTestXxxServiceIT 各自起容器。
  3. 共享 MySQL 后出现互相干扰风险
    • BaseR2dbcTest 的 repository 测试在 @BeforeEachDROP/CREATE TABLE,可能影响全链路 IT。

优化步骤#

1)把"伪集成测试"移回单测阶段#

将文件从:

  • GameCodeServiceCacheIT.java

改为:

  • GameCodeServiceCacheTest.java

对应类名也同步改为 GameCodeServiceCacheTest

代码片段:

@ExtendWith(MockitoExtension.class)
class GameCodeServiceCacheTest {
  // pure mock test
}

2)统一 Testcontainers:一次启动,多处复用#

新增共享容器类:SharedTestContainers

public final class SharedTestContainers {

  public static final String MAIN_DATABASE = "testdb";
  public static final String R2DBC_DATABASE = "testdb_r2dbc";

  public static final MySQLContainer<?> MYSQL =
      new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
          .withDatabaseName(MAIN_DATABASE)
          .withUsername("test")
          .withPassword("test")
          .withInitScript("schema.sql");

  public static final KafkaContainer KAFKA =
      new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"));

  public static final RedisContainer REDIS =
      new RedisContainer(DockerImageName.parse("redis:7.4.2"));

  static {
    Startables.deepStart(MYSQL, KAFKA, REDIS).join();
    createR2dbcDatabase();
  }

  private static void createR2dbcDatabase() {
    String rootUrl =
        String.format("jdbc:mysql://%s:%d/", MYSQL.getHost(), MYSQL.getFirstMappedPort());
    try (Connection conn = DriverManager.getConnection(rootUrl, "root", MYSQL.getPassword());
        Statement stmt = conn.createStatement()) {
      stmt.execute("CREATE DATABASE IF NOT EXISTS " + R2DBC_DATABASE);
      stmt.execute(
          "GRANT ALL PRIVILEGES ON "
              + R2DBC_DATABASE
              + ".* TO '"
              + MYSQL.getUsername()
              + "'@'%'");
    } catch (Exception e) {
      throw new RuntimeException("Failed to create R2DBC test database", e);
    }
  }
}

3)BaseIntegrationTest 切到共享容器#

@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
  var mysql = SharedTestContainers.MYSQL;
  var kafka = SharedTestContainers.KAFKA;
  var redis = SharedTestContainers.REDIS;

  registry.add("DB_CONNECTION_STRING",
      () -> String.format("%s:%d/%s", mysql.getHost(), mysql.getFirstMappedPort(), mysql.getDatabaseName()));
  registry.add("KAFKA_BOOTSTRAP_SERVERS", kafka::getBootstrapServers);
  registry.add("spring.data.redis.host", redis::getHost);
  registry.add("spring.data.redis.port", redis::getFirstMappedPort);
}

4)BaseR2dbcTest 使用隔离库,避免 DDL 冲突#

public abstract class BaseR2dbcTest {

  protected static final MySQLContainer<?> mysql = SharedTestContainers.MYSQL;

  @TestConfiguration
  static class TestR2dbcConfig extends AbstractR2dbcConfiguration {

    @Override
    @Bean
    @Primary
    @NonNull
    public ConnectionFactory connectionFactory() {
      String r2dbcUrl =
          String.format(
              "r2dbc:mysql://%s:%d/%s",
              mysql.getHost(),
              mysql.getFirstMappedPort(),
              SharedTestContainers.R2DBC_DATABASE);

      return ConnectionFactoryBuilder.withUrl(r2dbcUrl)
          .username(mysql.getUsername())
          .password(mysql.getPassword())
          .build();
    }
  }
}

这一步非常关键:共享同一个 MySQL 容器没问题,但必须把"会做 DROP/CREATE 的 repository 测试"放到独立 schema。


结果#

性能收益#

  • 集成测试阶段:约 4:02 -> 2:50
  • 全量构建:约 3:07(含单测+集成测试)

过程中的踩坑#

  1. 共享 MySQL 后大量 SC_XXX_WAITING_TIMEOUT

    • 根因:R2DBC repository 测试的 DDL 改表影响了全链路 IT
    • 解决:拆分独立数据库 testdb_r2dbc
  2. 创建数据库时报权限错误

    • Access denied for user 'test'@'%'
    • 解决:用 root 连接执行 CREATE DATABASE / GRANT

可复用的优化清单#

  • 纯 mock 测试不要放在 *IT.java
  • Testcontainers 尽量统一为共享单例
  • 若 repository 测试涉及 DDL,和全链路测试分 schema(同容器即可)
  • 修复后务必跑一次完整构建验证端到端