TestContainers集成测试环境

wen java案例 1

TestContainers 集成测试环境详解

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

TestContainers集成测试环境

核心概念

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 的优势:

  1. 真实环境测试:使用真实服务而非模拟
  2. 易于配置:一键启动所需依赖
  3. 环境隔离:测试间互不影响
  4. 自动清理:测试完成后自动销毁容器
  5. 广泛应用:支持各种数据库、消息队列、缓存等

最佳实践:

  • 将 TestContainers 配置抽象到基类
  • 合理使用 @ClassRule@DynamicPropertySource
  • 配置合适的超时和等待策略
  • 考虑容器复用以提升性能
  • 在 CI/CD 环境中确保 Docker 可用

抱歉,评论功能暂时关闭!