集成测试与 Testcontainers
本教程共 48 篇 · 第 41 篇 · 更新于 2026-08-13 · 约 4 分钟阅读
本节目标:掌握
@DataJpaTest的用法与局限,会用 Testcontainers 起真实数据库容器做集成测试,理解@ServiceConnection和@DynamicPropertySource。
从 Repository 测试说起
Web 层测完了,数据层怎么测?Repository 接口的方法名、@Query 注解的 SQL,都得验证。
Spring Boot 提供 @DataJpaTest 切片:只加载 JPA 相关组件。
- 扫描
@Entity实体类。 - 配置 Spring Data JPA 的 Repository。
- 类路径上有内嵌数据库就自动配置一个。
- 每个测试默认走事务,跑完自动回滚,数据不残留。
package com.example.demo.book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void findByIsbnReturnsBook() {
entityManager.persistAndFlush(new Book("Spring Boot 入门", "978-7-111-00001"));
Optional<Book> found = bookRepository.findByIsbn("978-7-111-00001");
assertThat(found).isPresent();
assertThat(found.get().getTitle()).isEqualTo("Spring Boot 入门");
}
}
TestEntityManager 是测试专用的 EntityManager,persistAndFlush 立即落库,确保查询能查到。
Note4.x 的
@DataJpaTest包名是org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest,旧版org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest是 3.x 写法。
内嵌数据库的问题
@DataJpaTest 默认用 H2 内嵌库。测试飞快,不用装任何东西。但隐患藏在细节里:
- H2 的方言和 MySQL / PostgreSQL 有差异。
- 生产用的 SQL 特性,H2 可能不支持或行为不同。
- 分页、锁、JSON 字段、全文索引,最容易踩坑。
内嵌库测试全绿,上了生产就报错,是经典事故。解法是用真实数据库测试。
Testcontainers 是什么
Testcontainers 是一个 Java 库:测试跑起来时,自动用 Docker 启动一个真实服务容器,测完自动销毁。数据库、Redis、Kafka 都能起。
先加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
前提:本机装有 Docker,且 Docker 守护进程在运行。测试会先拉镜像,第一次稍慢,之后走本地缓存。
第一种接法:@ServiceConnection
Spring Boot 3.1 起提供 @ServiceConnection。给容器字段加上它,Spring Boot 自动读取容器的地址、账号、密码,配置好数据源:
package com.example.demo.book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
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 static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class BookRepositoryContainerTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private BookRepository bookRepository;
@Test
void saveAndFindBook() {
bookRepository.save(new Book("测试驱动开发", "978-7-111-00002"));
assertThat(bookRepository.findByIsbn("978-7-111-00002")).isPresent();
}
}
三个注解各干一件事:
@Testcontainers:启用 Testcontainers 的 JUnit 扩展,管理容器生命周期。@Container:标记静态字段,类里所有测试跑之前启动容器,跑完停止。@AutoConfigureTestDatabase(replace = Replace.NONE):关键。默认@DataJpaTest会换成内嵌数据库,这行告诉它别换,用@ServiceConnection提供的真实库。
第二种接法:@DynamicPropertySource
@ServiceConnection 覆盖不了所有场景(比如自定义容器、非标准端口)。这时用 @DynamicPropertySource 手动注入连接信息:
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BookRepositoryDynamicTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void datasourceProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
// 测试方法略,和上面一样
}
registry.add 的第一个参数是配置键,第二个是值的提供者(等容器启动后才取值)。这招对任意属性都管用,比如 Redis 地址、Kafka 端口。
Note
@DynamicPropertySource是 Spring Framework 提供的通用机制,不依赖 Testcontainers。容器启动后,Spring 环境里就多了这几个属性,优先级高于application.yml。
整应用集成测试:@SpringBootTest + 容器
Repository 测完,还可以把整个应用拉起来,用容器数据库跑端到端验证:
package com.example.demo.book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
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;
@SpringBootTest
@Testcontainers
class BookIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private BookService bookService;
@Test
void serviceWorksWithRealDatabase() {
Book saved = bookService.create(new Book("云原生 Java", "978-7-111-00003"));
assertThat(saved.getId()).isNotNull();
}
}
@ServiceConnection 在 @SpringBootTest 下同样生效:数据源、JPA、Flyway 全部自动指向容器里的数据库。应用连的是真 PostgreSQL,测试价值直接拉满。
容器生命周期:谁管更靠谱
Testcontainers 的 JUnit 扩展(@Testcontainers + @Container)在测试类结束后就停容器。问题来了:Spring 会缓存应用上下文给多个测试类复用,上下文里的 Bean 还连着已停止的容器,后面再跑就可能报错。
官方建议:容器由 Spring 管理,而不是 JUnit 扩展。把容器声明成 Spring Bean:
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
@TestConfiguration(proxyBeanMethods = false)
class TestContainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>("postgres:16-alpine");
}
}
容器 Bean 会在其他 Bean 之前创建启动,在所有 Bean 销毁之后停止。需要它的测试类用 @Import(TestContainersConfiguration.class) 导入即可。
Warning跑 Testcontainers 测试前确认 Docker 在运行。否则测试直接报 “Could not find a valid Docker environment”。CI 环境记得装 Docker 或配置好 Docker 服务。
小结
@DataJpaTest用内嵌库测 Repository,快但方言有差异。- Testcontainers 用 Docker 起真实数据库,贴近生产。
@ServiceConnection自动配置连接,@DynamicPropertySource手动注入属性。- 容器最好交给 Spring 管理(Bean 方式),避免上下文缓存和容器生命周期冲突。