TestContainers 集成测试环境详解
TestContainers 是一个 Java 测试库,用于在测试中提供轻量级、一次性使用的容器实例,它支持 Docker 容器,可以在测试中自动启动数据库、消息队列、Web 服务器等依赖服务。

核心概念
graph LR
A[测试代码] --> B[TestContainers]
B --> C[Docker环境]
C --> D[PostgreSQL容器]
C --> E[Redis容器]
C --> F[Kafka容器]
D --> G[测试数据]
E --> H[缓存测试]
F --> I[消息测试]
基础配置
Maven 依赖
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- 数据库特定 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- 消息队列 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>redis</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
核心使用模式
1 单次容器模式(@ClassRule)
import org.junit.ClassRule;
import org.junit.Test;
import org.testcontainers.containers.PostgreSQLContainer;
public class SingleContainerTest {
@ClassRule
public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Test
public void testDatabaseConnection() {
String jdbcUrl = postgres.getJdbcUrl();
String username = postgres.getUsername();
String password = postgres.getPassword();
// 使用这些连接信息进行测试
try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {
assertNotNull(conn);
}
}
}
2 每次测试新容器(@Rule)
import org.junit.Rule;
import org.junit.Test;
public class FreshContainerTest {
@Rule
public PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Test
public void testFreshDatabase() {
// 每次测试都使用新的容器实例
}
}
实战示例:Spring Boot 集成测试
1 配置抽象测试基类
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@SpringBootTest
public abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
2 具体测试类
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
@AutoConfigureMockMvc
public class UserControllerTest extends AbstractIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Test
public void testCreateUser() throws Exception {
// 准备测试数据
User user = new User();
user.setName("Test User");
user.setEmail("test@example.com");
// 执行测试
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Test User"));
// 验证数据持久化
assertThat(userRepository.count()).isEqualTo(1);
}
@Test
public void testGetAllUsers() throws Exception {
// 插入测试数据
userRepository.save(new User("John", "john@example.com"));
userRepository.save(new User("Jane", "jane@example.com"));
// 执行测试
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
}
多容器组合测试
1 复杂场景:微服务测试
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import java.time.Duration;
@Testcontainers
public class MicroserviceIntegrationTest {
private static final Network network = Network.newNetwork();
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withNetwork(network)
.withNetworkAliases("database");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withNetwork(network)
.withNetworkAliases("cache")
.withExposedPorts(6379);
@Container
static GenericContainer<?> kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.0.0"))
.withNetwork(network)
.withNetworkAliases("message-broker");
@Container
static GenericContainer<?> app = new GenericContainer<>("myapp:latest")
.withNetwork(network)
.withExposedPorts(8080)
.withEnv("SPRING_DATASOURCE_URL", "jdbc:postgresql://database:5432/testdb")
.withEnv("SPRING_REDIS_HOST", "cache")
.withEnv("SPRING_KAFKA_BOOTSTRAP_SERVERS", "message-broker:9092")
.waitingFor(Wait.forHttp("/actuator/health")
.forStatusCode(200)
.withStartupTimeout(Duration.ofSeconds(60)));
@Test
public void testMicroserviceIntegration() {
// 测试完整的服务集成
String appUrl = String.format("http://%s:%d/api/test",
app.getHost(), app.getMappedPort(8080));
ResponseEntity<String> response = restTemplate.getForEntity(appUrl, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
进阶用法
1 自定义容器配置
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
public class CustomPostgreSQLContainer extends PostgreSQLContainer<CustomPostgreSQLContainer> {
private static final String IMAGE_VERSION = "postgres:15";
private static final String DATABASE_NAME = "integration_test";
public CustomPostgreSQLContainer() {
super(DockerImageName.parse(IMAGE_VERSION));
withDatabaseName(DATABASE_NAME);
withUsername("integration");
withPassword("integration");
withInitScript("sql/init-test-data.sql");
withExtraHost("custom-host", "127.0.0.1");
withLogConsumer(frame -> System.out.println(frame.getUtf8String()));
}
}
2 初始化脚本
-- src/test/resources/sql/init-test-data.sql
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES
('Admin', 'admin@test.com'),
('User', 'user@test.com');
3 数据源配置
@Configuration
@TestConfiguration
public class TestDataSourceConfig {
@Bean
@Primary
public DataSource dataSource(PostgreSQLContainer<?> postgres) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(postgres.getJdbcUrl());
config.setUsername(postgres.getUsername());
config.setPassword(postgres.getPassword());
config.setMaximumPoolSize(5);
config.setMinimumIdle(2);
return new HikariDataSource(config);
}
}
性能优化技巧
1 容器复用
import org.testcontainers.containers.GenericContainer;
public class ReusableContainerTest {
static GenericContainer<?> nginx = new GenericContainer<>("nginx:alpine")
.withExposedPorts(80)
.withReuse(true); // 启用容器复用
@BeforeAll
static void setUp() {
// 尝试重用已存在的容器
nginx.start();
}
}
2 并行测试配置
# testcontainers.properties testcontainers.reuse.enable=true testcontainers.parallel=true testcontainers.parallelism=8
常见问题解决
1 Docker 环境检查
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
@EnabledIfSystemProperty(named = "docker.available", matches = "true")
public class DockerDependentTest {
// 仅在 Docker 可用时运行
}
2 容器启动超时处理
@Container
static GenericContainer<?> slowContainer = new GenericContainer<>("slow-image:latest")
.withStartupTimeout(Duration.ofMinutes(5))
.waitingFor(Wait.forLogMessage(".*Started.*", 1));
TestContainers 的优势:
- 真实环境测试:使用真实服务而非模拟
- 易于配置:一键启动所需依赖
- 环境隔离:测试间互不影响
- 自动清理:测试完成后自动销毁容器
- 广泛应用:支持各种数据库、消息队列、缓存等
最佳实践:
- 将 TestContainers 配置抽象到基类
- 合理使用
@ClassRule和@DynamicPropertySource - 配置合适的超时和等待策略
- 考虑容器复用以提升性能
- 在 CI/CD 环境中确保 Docker 可用