本节目标:掌握 @SpringBootTest 的 webEnvironment 取值与 4.x 的 TestRestTemplate 变更,理解上下文缓存与 @DirtiesContext,学会三种测试数据隔离策略,并能用 Testcontainers 接入真实数据库。
适用版本:Spring Boot 4.1.x(Java 21)
17.3 集成测试
单元测试证明「Service 的算法对」,切片测试证明「Controller 的映射对、Repository 的查询对」,但它们都是各测各的。真实系统里这些层是串起来工作的:请求进来经 Controller 调 Service、Service 落库、事务提交、响应序列化回去。集成测试就是验证这条完整链路。
17.3.1 @SpringBootTest 与 webEnvironment
@SpringBootTest 会创建与 SpringApplication 启动时几乎一致的完整应用上下文。它有一个关键属性 webEnvironment,决定「要不要真起服务器」:
| 取值 | 行为 | 适用场景 |
|---|---|---|
MOCK(默认) | 不启动真实服务器,提供模拟的 Servlet 环境 | 只想装配全部 Bean,再用 MockMvc 发请求 |
RANDOM_PORT | 在随机端口启动真实内嵌服务器 | 需要真实 HTTP,集成测试首选 |
DEFINED_PORT | 用配置里的端口(默认 8080) | 少见,容易与本机进程冲突 |
NONE | 不创建 Web 环境 | 非 Web 应用(批处理、命令行) |
RANDOM_PORT 之所以是首选,是因为它避开了端口冲突:每次测试用一个空闲端口,本机同时跑多个测试也不会撞车。配它写出来的集成测试:
package com.example.library;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureTestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import com.example.library.domain.Book;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate
class BookApiIT {
@Autowired
private TestRestTemplate restTemplate;
@Test
void createThenGet_returnsCreatedBook() {
Book created = restTemplate.postForObject(
"/api/books",
new Book(null, "Effective Java", "Joshua Bloch", 2018),
Book.class);
ResponseEntity<Book> got = restTemplate.getForEntity(
"/api/books/" + created.getId(), Book.class);
assertThat(got.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(got.getBody().getTitle()).isEqualTo("Effective Java");
}
}
类名用 ...IT 结尾是常见约定:*Test 由 surefire 在测试阶段跑,*IT 交给 failsafe 在集成测试阶段跑,两批分开,快慢分层。
17.3.2 4.x 变更:TestRestTemplate 不再自带
上例里有两个注解值得单独说:
@AutoConfigureTestRestTemplate:4.x 必须显式加。3.x 里@SpringBootTest会自动配好TestRestTemplate,4.x 取消了这层隐含。- 依赖:4.x 把
TestRestTemplate拆到了独立模块,需要额外引入它所在的测试模块依赖,spring-boot-starter-test不再默认带它。
如果不引入 TestRestTemplate,可以直接改用 17.2 介绍过的 RestTestClient——它在 4.x 里也能配合 @SpringBootTest 使用,并且无需额外依赖:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class BookApiRestTestClientIT {
@Autowired
private RestTestClient restTestClient;
@Test
void createThenGet() {
restTestClient.post().uri("/api/books")
.body(new Book(null, "Effective Java", "Joshua Bloch", 2018))
.exchange()
.expectStatus().isCreated();
restTestClient.get().uri("/api/books/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.title").isEqualTo("Effective Java");
}
}
一句话选择:新项目直接用 RestTestClient;已有代码迁移时按需引入 TestRestTemplate 模块。
17.3.3 为什么 @SpringBootTest 慢:上下文缓存
@SpringBootTest 慢的根源是「装配整个上下文」:扫描 Bean、建数据源、建 EntityManagerFactory、建 Tomcat。单看一次要接近一秒(第 3 章实测 0.8–1.1 秒)。
但 Spring TestContext 框架有一个上下文缓存:它按一组「配置键」缓存已建好的 ApplicationContext,配置相同的测试类会复用同一个上下文,不再重复装配。配置键包括:
webEnvironment取值- 激活的 profile
@TestPropertySource/@DynamicPropertySource注入的属性- 存在的
@MockitoBean/@MockitoSpyBean及其类型 - 上下文自定义器、
@ContextConfiguration
所以让测试变快的关键,是尽量让更多测试类共享同一套配置。 反例:给每个测试类都加一个不同的 @TestPropertySource,缓存命中率为零,每个类都重新装配一次。正例:统一用 RANDOM_PORT + 同一套 profile,几十个测试类只付一次启动成本。
17.3.4 @DirtiesContext:主动让缓存失效
有时候测试会改动上下文里的状态,导致后续复用它的测试看到脏数据。这时用 @DirtiesContext 标记「这个上下文被弄脏了,用完请销毁重建」:
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class MutableConfigTest {
// 类执行完后销毁上下文
}
@DirtiesContext 是代价很高的注解:销毁并重建上下文意味着后续测试要重新装配一次。只在确有必要时用(例如测试修改了某个单例 Bean 的内部状态),不要为了「保险」给所有测试类都加上——那等于把缓存机制废掉。
17.3.5 测试数据隔离策略
集成测试连的是真数据库(或 Testcontainers),测试之间的数据必须隔离,否则用例顺序一变就失败。三种策略:
| 策略 | 做法 | 优点 | 缺点 |
|---|---|---|---|
@Transactional 回滚 | 测试方法包在事务里,结束回滚 | 零清理代码 | 对 RANDOM_PORT 的真实 HTTP 请求无效(请求走独立连接,不在同一事务里) |
@Sql 脚本 | 用 SQL 脚本准备/清理数据 | 可控、可读 | 要维护脚本 |
| Testcontainers | 每个测试类/套件起一个干净数据库 | 真方言、隔离彻底 | 启动容器有开销 |
最容易踩的坑是第一行:很多人习惯给集成测试类加 @Transactional 期望自动回滚,但在 RANDOM_PORT 下请求经由真实 HTTP 打到另一个线程、另一条连接,事务根本不共享,数据照样落库。此时应改用 @Sql 或在 @BeforeEach/@AfterEach 里手动清理。
@Sql 的用法:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class BookApiSqlIT {
// 每个测试方法后执行 cleanup.sql 清库
}
17.3.6 @DynamicPropertySource 注入动态配置
当配置值要等到运行时才知道(典型场景:Testcontainers 启动后才知道数据库端口),用 @DynamicPropertySource 注册:
@DynamicPropertySource
static void registerProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
它接收一个 DynamicPropertyRegistry,用 add(属性名, 取值 Supplier) 注册。值用 Supplier 懒取,因为注册发生在容器启动前,真正的 URL 要等容器起来才有。这是 Testcontainers 手动接线的标准写法。
17.3.7 Testcontainers 2.0 接入真实数据库
内嵌 H2 的方言与生产库(如 PostgreSQL)并不完全一致,有些 SQL 在 H2 上过、在真库上挂。要验证真实方言,用 Testcontainers 起一个一次性数据库容器。4.x 依赖的是 Testcontainers 2.0。
最省事的写法是靠 @ServiceConnection——它会自动把容器的连接信息接到 Spring Boot 的数据源配置上,无需手写 @DynamicPropertySource:
package com.example.library;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureTestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.example.library.domain.Book;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate
@Testcontainers
class BookApiContainerIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");
@Autowired
private TestRestTemplate restTemplate;
@Test
void createBook_persistsToRealPostgres() {
Book created = restTemplate.postForObject(
"/api/books",
new Book(null, "Effective Java", "Joshua Bloch", 2018),
Book.class);
assertThat(created.getId()).isNotNull();
assertThat(restTemplate.getForObject("/api/books/" + created.getId(), Book.class)
.getTitle()).isEqualTo("Effective Java");
}
}
要点:
@Testcontainers让 JUnit 5 管理容器生命周期,@Container标记容器字段,static表示整个类共用一个容器。@ServiceConnection是 Spring Boot 的桥接注解:容器一启动,它就把 JDBC URL、用户名、密码注册进数据源配置。这比手写@DynamicPropertySource更简洁,也是当前推荐方式。- Testcontainers 依赖本机装了 Docker;CI 上需要能跑容器。
如果不用 @ServiceConnection(例如需要额外自定义属性),退回 17.3.6 的 @DynamicPropertySource 手动接线即可。
17.3.8 @TestConfiguration 与 @TestBean 覆盖 Bean
集成测试有时需要把一个真实 Bean 换成测试专用版本(如固定时钟)。两种方式。
@TestConfiguration:定义一个只在测试里生效的配置类,用 @Bean 提供替代实现。
@SpringBootTest
class ClockTest {
@TestConfiguration
static class FixedClockConfig {
@Bean
Clock clock() {
return Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
}
}
}
注意它是静态内嵌类,且只提供增量 Bean;命名 FixedClockConfig 是为了避免与主配置重名。
@TestBean:Spring 4.x 更推荐的单 Bean 覆盖方式。在测试字段上标 @TestBean,并提供一个同名的静态工厂方法返回替代实现:
@SpringBootTest
class BookServiceBeanTest {
@TestBean
Clock clock;
static Clock clock() {
return Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
}
}
@TestBean 只覆盖上下文里已存在的那个 Bean,作用范围精确、不污染其他测试,且它能用在测试类里——正好补上了 17.2 提到的「@MockitoBean 不能放进 @Configuration」的缺口。选择标准:只是替换单个 Bean 就用 @TestBean;需要一组相关配置才用 @TestConfiguration。
17.3.9 三层测试选择速查表
把本章三节收进一张表:
| 你要验证的东西 | 用哪一层 | 关键注解 | 是否起容器 |
|---|---|---|---|
| 单个类的分支逻辑、算法 | 单元测试 | JUnit 5 + @ExtendWith(MockitoExtension.class) | 否 |
| Controller 的映射、参数绑定、JSON | 切片测试 | @WebMvcTest + @AutoConfigureMockMvc | 只 MVC |
| Repository 的派生查询、实体映射 | 切片测试 | @DataJpaTest | 只 JPA |
| 序列化/反序列化本身 | 切片测试 | @JsonTest | 只 Jackson |
| 跨层协作、事务、真实数据库方言 | 集成测试 | @SpringBootTest(RANDOM_PORT) + Testcontainers | 是 |
经验配比是金字塔形:单元测试占大头,切片居中,集成测试只保留最关键的几条端到端路径。集成测试不是越多越好——它慢、脆、定位难,应当把覆盖面让给下面两层。
小结
@SpringBootTest装配完整上下文;webEnvironment四取值中RANDOM_PORT是集成测试首选(避开端口冲突),NONE用于非 Web 应用。- 4.x 下
TestRestTemplate需显式加@AutoConfigureTestRestTemplate并引入相应依赖,或改用无额外依赖的RestTestClient。 - 集成测试慢在上下文装配;Spring TestContext 会按配置键缓存上下文,配置一致的测试类复用同一上下文,因此应尽量减少配置差异。
@DirtiesContext主动让缓存失效,代价高,只在测试会污染上下文状态时用。- 数据隔离三策略:
@Transactional回滚(对RANDOM_PORT的真实 HTTP 无效)、@Sql脚本、Testcontainers。 @DynamicPropertySource用 Supplier 懒取运行时才确定的配置,是 Testcontainers 手动接线的标准写法。- Testcontainers 2.0 +
@ServiceConnection自动把容器连接信息接入数据源,是验证真实数据库方言的推荐方式。 - 覆盖 Bean 优先用
@TestBean(可放在测试类里),需要一组配置时用静态内嵌的@TestConfiguration。
到这里,图书服务的三层测试就完整了:单元测试守住 Service 逻辑,切片测试守住 Controller 与 Repository,集成测试守住整条链路。测试写完之后,下一章我们回到应用本身,走一遍需求梳理、设计与打包发布的完整流程。
阅读导航:上一节:17.2 切片测试 · 下一节:18.1 需求与设计 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。