Spring Boot 测试与 Testcontainers 实战
单元测试用 H2 内存数据库跑得好好的,上线到 MySQL 就出问题?Testcontainers 让你在测试时启动一个真实的 Docker 数据库容器,测试环境和生产环境完全一致。本文从依赖配置到容器复用,讲清 Testcontainers 在 Spring Boot 中的实战用法。
一、集成测试的痛点
传统 Spring Boot 集成测试用 H2 内存数据库做替代。问题是 H2 不是 MySQL:自增主键写法不同、SQL 方言有差异、索引行为不一样。测试过了不代表生产过了。Testcontainers 的解决方案很直接:测试时用 Docker 启动一个和生产一模一样的 MySQL 容器,测试结束自动销毁。
| 方案 | 数据库 | 与生产一致 | 速度 |
|---|---|---|---|
| H2 内存库 | H2 | 不一致 | 快 |
| Testcontainers | MySQL/PG | 完全一致 | 慢但可靠 |
二、引入依赖
<!-- pom.xml -->
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
三、第一个 Testcontainers 测试
@SpringBootTest
@Testcontainers
class UserRepositoryTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User();
user.setName("小明");
user.setEmail("xm@example.com");
userRepository.save(user);
Optional<User> found = userRepository.findById(user.getId());
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("小明");
}
}
@Container 加 static 表示所有测试方法共享同一个容器,避免每个方法都启动一次。@DynamicPropertySource 把容器的连接信息动态注入 Spring 配置。
四、容器复用模式
每次测试启动 MySQL 要 10-20 秒,整个测试套件跑下来很慢。Testcontainers 提供了单例容器模式:整个测试类只启动一次容器。
// 单例容器:整个 JVM 只启动一次
public abstract class AbstractContainerBaseTest {
static final MySQLContainer<?> MYSQL;
static {
MYSQL = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
MYSQL.start();
}
@DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", MYSQL::getJdbcUrl);
registry.add("spring.datasource.username", MYSQL::getUsername);
registry.add("spring.datasource.password", MYSQL::getPassword);
}
}
// 所有集成测试继承这个基类
class UserServiceTest extends AbstractContainerBaseTest {
// 测试方法...
}
五、测试 REST API
结合 @SpringBootTest 和 TestRestTemplate,可以测试完整的 HTTP 接口。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserApiTest extends AbstractContainerBaseTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateUser() {
UserCreateRequest request = new UserCreateRequest("小红", "xh@example.com");
ResponseEntity<UserResponse> response = restTemplate.postForEntity(
"/users", request, UserResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getName()).isEqualTo("小红");
}
}
六、常见问题
Docker 没启动:报 Could not find a valid Docker environment。先启动 Docker Desktop。
容器启动慢:用单例容器模式 + 配置 Docker 镜像加速。CI 里可以用 Testcontainers 的 Cloud 功能。
数据残留:每个测试方法前用 @Transactional 回滚,或者用 @Sql 重置数据。
七、总结
Testcontainers 解决了集成测试最大的可信度问题:测试环境和生产环境用同一个数据库。代价是测试变慢,但换来的是"测试通过就能上线"的信心。建议核心业务路径的集成测试都用 Testcontainers,纯逻辑单元测试继续用 Mockito。