1240 字
约 4 分钟
15
S16-B03 · 集成测试:Spring Boot Test 与 Testcontainers

适用读者:mock 通过、真实环境失败过的人;要补"能证明 SQL/事务/序列化"的测试的人。
前置:S16-B02、S07(Spring)、S11(MySQL)、本机有 Docker。
覆盖旧文:无直接旧卡(E 驱动)。依据:Spring Boot Reference「Testing」章、Testcontainers for Java 文档。
扩展点:E-S16-003(测试切片的选择)。
上一篇:S16-B02 · 单元测试:JUnit 5 + Mockito 的最小可运行
下一篇:S16-B04 · 端到端。


1. 一句话结论

集成测试的价值不在"更真实",在于它把被 mock 排除掉的那部分(SQL、事务、序列化、配置绑定)重新纳入证据。 代价是慢,所以要按切片选范围,而不是"全都 @SpringBootTest"。

你会漏掉的缺陷 只有集成能测
SQL 写错(WHERE 条件、类型映射) ✅
事务边界(异常是否回滚) ✅
JSON 序列化(字段名、时间格式) ✅(切片)
Bean 装配(循环依赖、条件装配) ✅
分布式锁/幂等 ❌ 需要 E2E 或多实例(B04)

2. 环境(2026-11 实测)

// build.gradle — Spring Boot 3.3.6 / Java 17
dependencies {
    testImplementation "org.springframework.boot:spring-boot-starter-test"      // JUnit5 + AssertJ + Mockito
    testImplementation "org.testcontainers:junit-jupiter:1.20.4"
    testImplementation "org.testcontainers:mysql:1.20.4"
    testImplementation "com.mysql:mysql-connector-j:8.4.0"
}
@SpringBootTest
@Testcontainers
class OrderFlowIT {   // 注意:*IT 由 failsafe 跑,不混进单测时长
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4")
            .withDatabaseName("shop").withUsername("u").withPassword("p");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("spring.datasource.url", mysql::getJdbcUrl);
        r.add("spring.datasource.username", mysql::getUsername);
        r.add("spring.datasource.password", mysql::getPassword);
    }
    // ... 注入 OrderService / JdbcTemplate 断言
}

预期输出(首次运行会拉镜像,约 20–60s;之后 3–8s):

$ ./gradlew integrationTest
OrderFlowIT > 库存不足时整单回滚() PASSED
2 tests completed

3. 切片选择(E-S16-003)

注解 启动内容 适合证明 启动耗时(量级)
@WebMvcTest(X.class) 仅 Web 层 + 指定 Controller 路由、参数校验、状态码、JSON 结构 1–3s
@DataJpaTest JPA + 数据源 映射、查询方法、约束 2–5s(+容器)
@JdbcTest JdbcTemplate + 数据源 手写 SQL、事务 2–5s
@SpringBootTest(webEnvironment=RANDOM_PORT) 全上下文 装配、事务、跨层调用 5–15s
@SpringBootTest + Testcontainers 全上下文 + 真依赖 上述全部 + 真实 SQL 方言 8–30s

判据:能用切片就不用全量。全量测试每多一个,CI 时长线性增长,最终会被团队"跳过"。


4. 两个最容易踩的坑

4.1 @Transactional 让"回滚"永远为真

@Transactional   // 测试结束后自己回滚
@Test
void 库存不足时整单回滚() { ... }

测试方法上的 @Transactional 会让整个用例在一个事务里跑,被测代码的"提交/回滚"被外层吞掉——于是"回滚"这条断言永远成立,等于没测。

改法:去掉测试方法上的 @Transactional,用 @Sql 或显式清理;或在断言前用 JdbcTemplate 直查(能看到未提交数据说明事务没生效)。

4.2 用 H2 代替 MySQL 证明方言

差异 H2 MySQL 8
自增与锁 行为不同 AUTO_INCREMENT、间隙锁
唯一索引 + NULL 多 NULL 允许 多 NULL 允许(易误解为唯一)
时间精度 微秒/纳秒差异 DATETIME(3)
大小写敏感 依配置 依 collation

判据:凡断言涉及锁、事务隔离、排序、字符集,必须真库;纯 CRUD 映射可以用 H2,并在类注释写明"未覆盖 MySQL 方言"。


5. 一个可跑的完整例子

@SpringBootTest
@Testcontainers
class StockIntegrationTest {

    @Autowired InventoryService service;
    @Autowired JdbcTemplate jdbc;

    @Test
    void 扣减失败时库存不变() {
        jdbc.update("insert into stock(sku_id, qty) values (1, 1)");
        assertThatThrownBy(() -> service.deduct(1L, 2))
            .isInstanceOf(InsufficientStockException.class);
        Integer left = jdbc.queryForObject("select qty from stock where sku_id=1", Integer.class);
        assertThat(left).isEqualTo(1);                  // 关键:真库直查
    }
}

这条用例只有集成层能写:单元层需要 mock repo.decrease,而实现中的 SQL 条件(WHERE qty >= ?)正是缺陷高发处。


6. 常见误区

误区 纠正
"把单测全改成 @SpringBootTest 更保险" 时长×10,反馈变慢,最终被跳过
"容器测试不稳定,不如 H2" 不稳定的是端口/镜像,不是容器本身;用 RANDOM_PORT + 固定镜像 tag
"集成测试要覆盖所有分支" 覆盖契约与 SQL 即可;分支逻辑归单元层
"测试里可以随便改库" 每个用例自带数据、自己清理;不要依赖执行顺序

7. 自测

  1. 你的项目里有几个 @SpringBootTest?能不能降成切片?
  2. 有没有测试方法上挂着 @Transactional 却在断言回滚?
  3. 你的 SQL 断言跑在 H2 还是 MySQL?如果是 H2,哪些结论不能外推?

8. 下一篇

S16-B04 · 端到端:跑通完整流程才算真正验证 —— 从"agent 说做完了"到"系统级证据",含四类组件边界缺陷与可执行 harness。

S16-B03 · 集成测试:Spring Boot Test 与 Testcontainers
https://www.clxhxhhr.top/posts/5408/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。