集成测试怎么写才不慢?Spring Boot + Testcontainers 的踩坑对比和参数清单

🔑 关键词:集成测试,Testcontainers,Spring Boot,接口测试,测试金字塔

📖 摘要:一篇偏实战的集成测试笔记:对比 @SpringBootTest、@DataJpaTest、H2 和 Testcontainers,记录我项目里 CI 从 27 分钟压到 9 分钟的具体参数、步骤和常见报错。

去年做跨境结算订单服务,23 个集成测试把 CI 从 11 分钟拖到 27 分钟。最开始我们图快,用 H2 2.2.224 顶 PostgreSQL 16.2,结果上线前发现 JSONB 字段的 GIN 索引根本没测到,ON CONFLICT DO UPDATE 在 H2 里语法都不一样,时区 Asia/Shanghai 写进去变成 UTC,财务对账差 8 小时。那次周五晚上修到凌晨两点,从此我不信“内存数据库能替代集成测试”这句话。后来换 Testcontainers 1.19.7 + postgres:16.2-alpine,配合容器复用和 Failsafe 3.2.5 拆分,CI 回到 9 分 12 秒。不是所有测试都变快,是大部分测试不再重复启动 Spring 上下文。

图片

我的独立观点:集成测试按“契约切片”切,别按 Controller/Service/DAO 切

很多教程一上来就 @SpringBootTest 全量启动,然后 MockMvc 打 Controller,这其实是在写“慢速的单元测试”。我现在把集成测试切成三种契约切片:1)HTTP 入参出参 + 鉴权 + 异常码,用 TestRestTemplate 或 RestAssured 5.4.0,端口 RANDOM_PORT,断言 201、400、409、422 这些业务码;2)数据库事务与约束,用 @DataJpaTest + Testcontainers,验证唯一索引、外键、乐观锁 version、Flyway 9.22.3 迁移后的表结构;3)外部依赖契约,第三方支付用 WireMock 3.3.1,Kafka 用 confluentinc/cp-kafka:7.5.0,Redis 用 redis:7.2.4。每个切片只碰 1 到 2 个真实依赖,其余能 stub 就 stub。我们团队比例大概是单元 62%、集成 31%、E2E 7%,集成里 80% 是 @SpringBootTest + Testcontainers,20% 是 @DataJpaTest + Testcontainers。别追求 80% 集成覆盖率,那个数字会把 CI 拖死。

对比:H2、@DataJpaTest、@SpringBootTest、Testcontainers 到底怎么选

图片

| 维度 | H2 2.2.224 | PostgreSQL 16.2-alpine + Testcontainers | @DataJpaTest | @SpringBootTest | | 启动耗时 | 0.2 秒左右 | 首次 3 到 8 秒,复用后 0.5 到 1 秒 | 1 到 2 秒 | 5 到 15 秒,取决于上下文缓存 | | SQL 兼容 | JSONB、RETURNING、ON CONFLICT 容易翻车 | 和生产一致 | 只适合 Repository 层 | 全链路,但容易变慢 | | 事务 | 默认回滚 | 不回滚,需要手动清 | 默认回滚 | 不回滚 | | 适合场景 | 早期冒烟 | 核心数据库契约 | 约束、索引、查询 | HTTP + 数据库 + MQ 组合 |

这张表不是让你二选一。我的做法是:单元测试继续用 Mockito 5.10.0,Repository 的复杂查询用 @DataJpaTest + PostgreSQL 容器,Controller 到 DB 的核心链路用 @SpringBootTest(webEnvironment = RANDOM_PORT),第三方 HTTP 全部 WireMock。Hikari 参数改成 maximumPoolSize=10、connectionTimeout=3000、validationTimeout=1000,不然容器启动慢时连接池会先报 timeout。

图片

可抄步骤:从 0 到 1 搭一个不慢的集成测试

  1. 依赖:org.testcontainers:junit-jupiter:1.19.7、org.testcontainers:postgresql:1.19.7、org.testcontainers:kafka:1.19.7、org.springframework.boot:spring-boot-testcontainers:3.2.5、org.junit.jupiter:junit-jupiter:5.10.2、org.assertj:assertj-core:3.25.3。Maven 用 Failsafe 3.2.5 跑 IT.java,Surefire 只跑 Test.java。

  2. 基类写 @Testcontainers、@SpringBootTest(webEnvironment = RANDOM_PORT)、@ActiveProfiles("integration"),static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16.2-alpine").withDatabaseName("app_test").withUsername("test").withPassword("test").withReuse(true)。@DynamicPropertySource 里把 spring.datasource.url 指向 postgres.getJdbcUrl(),再设置 spring.jpa.hibernate.ddl-auto=validate,让 Flyway 管表结构。

图片

  1. 本地复用容器:在 ~/.testcontainers.properties 写 testcontainers.reuse.enable=true,CI 加 -Dtestcontainers.reuse.enable=true。但注意 GitHub Actions 每次新 runner 都无效,复用只适合本地和固定 runner。

  2. 数据清理别用 @Transactional 包 HTTP 测试。我们写 clean.sql:TRUNCATE TABLE order_item, orders, users RESTART IDENTITY CASCADE;然后 @Sql(scripts = "/clean.sql", executionPhase = BEFORE_TEST_METHOD)。测试数据用 UUID 后缀,别写死 userId=1,并行时会互相踩。

图片

  1. Kafka 等待别用 Thread.sleep。用 Awaitility 4.2.0:await().atMost(30, SECONDS).pollInterval(200, MILLISECONDS).until(() -> consumer.poll(Duration.ofMillis(100)).count() == 1)。KafkaContainer 超时设 60 秒,镜像锁 confluentinc/cp-kafka:7.5.0。

  2. 并行参数:junit-platform.properties 里 junit.jupiter.execution.parallel.enabled=true、junit.jupiter.execution.parallel.mode.default=concurrent、junit.jupiter.execution.parallel.config.strategy=fixed、junit.jupiter.execution.parallel.config.fixed.parallelism=4。但 Spring 上下文配置不同的测试类别并行,否则缓存从 2 个爆到 10 个,内存直接上去。

  3. 报错排查:Could not find a valid Docker environment 先看 DOCKER_HOST,Mac 用 Colima 时 export DOCKER_HOST=unix://$HOME/.colima/default/docker.sock;Timed out waiting for container port 把 withStartupTimeout(Duration.ofSeconds(60)) 加上;Ryuk 权限问题在 CI 里挂 /var/run/docker.sock,实在不行临时 TESTCONTAINERS_RYUK_DISABLED=true,但别当长期方案。

图片

最后说点得罪人的:Pact、EmbeddedKafka、全量 E2E 都别神化

Pact 4.6.14 做消费者驱动合同测试很好,但它不验证你的 SQL 和事务,替代不了集成测试。EmbeddedKafka 3.2.5 启动快,但版本漂移会骗你,我就遇到过本地 EmbeddedKafka 通过、测试环境 Kafka 3.7.0 因为 acks 配置不同丢消息。全量 E2E 更别天天跑,我们只保留 7 条,覆盖下单、支付回调、退款、对账四个主流程,跑一次 4 分 30 秒。集成测试的核心不是覆盖多少代码,而是把不可控的外部依赖变成可控契约。容器版本锁死,测试数据可重复,断言落在业务不变量上。我们最后只留 14 个集成测试,覆盖 9 个核心契约,CI 9 分钟,我晚上能睡好觉。文章有点散,但都是我踩过的坑。

🏷️ 标签: