Spring Boot Integration Test Optimization Practice
目录
背景#
在一次常规的集成测试构建中,发现集成测试整体耗时偏高,影响开发反馈速度。
项目技术栈是 Spring Boot + WebFlux + R2DBC + Reactive Redis + Kafka,测试侧大量使用 Testcontainers。
问题:集成测试为什么慢?#
观察结果#
- 单测(Surefire)总耗时约:
3.4s - 集成测试(Failsafe)总耗时约:
270s
瓶颈明显在 IT 阶段。
关键原因#
- 测试类型混跑:
- 一个纯 Mockito 测试命名为
*IT.java,被 Failsafe 执行。
- 一个纯 Mockito 测试命名为
- 容器启动重复:
BaseIntegrationTest、BaseR2dbcTest、XxxServiceIT各自起容器。
- 共享 MySQL 后出现互相干扰风险:
BaseR2dbcTest的 repository 测试在@BeforeEach做DROP/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(含单测+集成测试)
过程中的踩坑#
-
共享 MySQL 后大量
SC_XXX_WAITING_TIMEOUT- 根因:R2DBC repository 测试的 DDL 改表影响了全链路 IT
- 解决:拆分独立数据库
testdb_r2dbc
-
创建数据库时报权限错误
Access denied for user 'test'@'%'- 解决:用 root 连接执行
CREATE DATABASE / GRANT
可复用的优化清单#
- 纯 mock 测试不要放在
*IT.java - Testcontainers 尽量统一为共享单例
- 若 repository 测试涉及 DDL,和全链路测试分 schema(同容器即可)
- 修复后务必跑一次完整构建验证端到端
Read other posts