Skip to content
清晨的一缕阳光
返回

Spring Boot Testcontainers 测试实战

前言

Testcontainers 是一个 Java 库,支持在 Docker 容器中运行测试依赖。它提供了真实的测试环境,避免了 Mock 的不准确性。本文将介绍 Testcontainers 在 Spring Boot 测试中的完整应用。

快速开始

1. 添加依赖

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>

2. 基础使用

@Testcontainers
@SpringBootTest
class BasicTestcontainersTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @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 testWithMySQL() {
        User user = new User();
        user.setUsername("test");
        userRepository.save(user);
        
        assertEquals(1, userRepository.count());
    }
}

数据库容器

1. MySQL

@Testcontainers
@SpringBootTest
class MySQLTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.36")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test")
        .withInitScript("init.sql")
        .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci");
    
    @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);
        registry.add("spring.datasource.driver-class-name", mysql::getDriverClassName);
    }
    
    @Test
    void testMySQLIntegration() {
        // 测试逻辑
    }
}

2. PostgreSQL

@Testcontainers
@SpringBootTest
class PostgreSQLTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15.4")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test")
        .withInitScript("init-postgres.sql");
    
    @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);
    }
    
    @Test
    void testPostgreSQLIntegration() {
        // 测试逻辑
    }
}

3. MongoDB

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mongodb</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
@Testcontainers
@SpringBootTest
class MongoDBTest {
    
    @Container
    static MongoDBContainer mongo = new MongoDBContainer("mongo:7")
        .withDatabaseName("testdb");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl);
    }
    
    @Autowired
    private MongoTemplate mongoTemplate;
    
    @Test
    void testMongoDBIntegration() {
        User user = new User();
        user.setUsername("test");
        mongoTemplate.save(user);
        
        List<User> users = mongoTemplate.findAll(User.class);
        assertEquals(1, users.size());
    }
}

4. Redis

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>redis</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
@Testcontainers
@SpringBootTest
class RedisTest {
    
    @Container
    static RedisContainer<?> redis = new RedisContainer<>("redis:7.2")
        .withExposedPorts(6379);
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.redis.host", redis::getContainerIpAddress);
        registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
    }
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Test
    void testRedisIntegration() {
        redisTemplate.opsForValue().set("key", "value", 10, TimeUnit.MINUTES);
        
        String value = (String) redisTemplate.opsForValue().get("key");
        assertEquals("value", value);
    }
}

5. Elasticsearch

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
@Testcontainers
@SpringBootTest
class ElasticsearchTest {
    
    @Container
    static ElasticsearchContainer elasticsearch = new ElasticsearchContainer("elasticsearch:8.11.0")
        .withEnv("xpack.security.enabled", "false");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.elasticsearch.uris", () -> 
            List.of("http://" + elasticsearch.getHttpHostAddress()));
    }
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchTemplate;
    
    @Test
    void testElasticsearchIntegration() {
        // 测试逻辑
    }
}

消息队列容器

1. Kafka

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>kafka</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
@Testcontainers
@SpringBootTest
class KafkaTest {
    
    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
    );
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    @Test
    void testKafkaIntegration() throws Exception {
        kafkaTemplate.send("test-topic", "key", "value").get();
        
        // 消费验证
        // ...
    }
}

2. RabbitMQ

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>rabbitmq</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
@Testcontainers
@SpringBootTest
class RabbitMQTest {
    
    @Container
    static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.12-management");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.rabbitmq.host", rabbitmq::getContainerIpAddress);
        registry.add("spring.rabbitmq.port", () -> rabbitmq.getAmqpPort());
    }
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Test
    void testRabbitMQIntegration() {
        rabbitTemplate.convertAndSend("test-exchange", "routing-key", "message");
        
        // 消费验证
        // ...
    }
}

3. RocketMQ

@Testcontainers
@SpringBootTest
class RocketMQTest {
    
    @Container
    static GenericContainer<?> rocketmq = new GenericContainer<>("apache/rocketmq:5.1.0")
        .withExposedPorts(9876, 10911)
        .withCommand("sh", "mqnamesrv");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("rocketmq.name-server", () -> 
            rocketmq.getContainerIpAddress() + ":" + rocketmq.getMappedPort(9876));
    }
    
    @Test
    void testRocketMQIntegration() {
        // 测试逻辑
    }
}

容器编排

1. Docker Compose

@Testcontainers
@SpringBootTest
class DockerComposeTest {
    
    @Container
    static DockerComposeContainer compose = new DockerComposeContainer(
        new File("src/test/resources/docker-compose.test.yml")
    )
    .withExposedService("mysql", 3306)
    .withExposedService("redis", 6379);
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", () -> 
            "jdbc:mysql://" + compose.getServiceHost("mysql", 3306) + ":" + 
            compose.getServicePort("mysql", 3306) + "/testdb");
        
        registry.add("spring.redis.host", () -> 
            compose.getServiceHost("redis", 6379));
        registry.add("spring.redis.port", () -> 
            compose.getServicePort("redis", 6379));
    }
    
    @Test
    void testWithDockerCompose() {
        // 测试逻辑
    }
}

2. 自定义容器网络

@Testcontainers
@SpringBootTest
class NetworkTest {
    
    static Network network = Network.newNetwork();
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withNetwork(network)
        .withNetworkAliases("mysql");
    
    @Container
    static RedisContainer<?> redis = new RedisContainer<>("redis:7")
        .withNetwork(network)
        .withNetworkAliases("redis");
    
    @Container
    static GenericContainer<?> app = new GenericContainer<>("myapp:latest")
        .withNetwork(network)
        .withEnv("MYSQL_HOST", "mysql")
        .withEnv("REDIS_HOST", "redis")
        .dependsOn(mysql, redis);
    
    @Test
    void testWithNetwork() {
        // 测试容器间通信
    }
}

3. 多模块项目

// 父类定义共享容器
@Testcontainers
abstract class BaseContainerTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
    
    @Container
    static RedisContainer<?> redis = new RedisContainer<>("redis:7");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.redis.host", redis::getContainerIpAddress);
        registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
    }
}

// 子类继承
@SpringBootTest
class UserServiceTest extends BaseContainerTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    void testUserService() {
        // 使用共享容器
    }
}

高级用法

1. 容器快照

@Testcontainers
@SpringBootTest
class SnapshotTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withInitScript("init.sql");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
    }
    
    @Test
    void testWithSnapshot() {
        // 执行操作
        // 使用容器快照恢复
    }
}

2. 等待策略

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
    .waitingFor(Wait.forLogMessage(".*ready for connections.*", 1))
    .withStartupTimeout(Duration.ofMinutes(2));

@Container
static KafkaContainer kafka = new KafkaContainer(
    DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
)
    .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1));

3. 容器日志

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
    .withLogConsumer(outputFrame -> {
        String log = outputFrame.getUtf8String();
        System.out.println("MySQL: " + log);
    });

4. 临时文件挂载

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
    .withFileSystemBind("src/test/resources/mysql", "/docker-entrypoint-initdb.d");

最佳实践

1. 容器复用

// 使用静态容器,在测试类之间复用
@Testcontainers
class ReusableContainerTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withReuse(true); // 允许复用
}

2. 资源清理

@Testcontainers
@SpringBootTest
class CleanupTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
    
    @AfterAll
    static void cleanup() {
        // 清理容器资源
        mysql.stop();
    }
}

3. CI/CD 配置

# GitHub Actions
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Run tests with Testcontainers
        run: mvn test
        env:
          TESTCONTAINERS_RYUK_DISABLED: true

4. 性能优化

// 使用预构建镜像
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>(
    DockerImageName.parse("mysql:8.0")
    .asCompatibleSubstituteFor("mysql")
);

// 并行执行测试
@Test
@Execution(ExecutionMode.CONCURRENT)
void test1() { }

@Test
@Execution(ExecutionMode.CONCURRENT)
void test2() { }

总结

Testcontainers 要点:

Testcontainers 提供真实的集成测试环境。


分享这篇文章到:

上一篇文章
Sentinel 控制台实战
下一篇文章
Hertz 高性能框架