前言
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 要点:
- ✅ 数据库容器 - MySQL、PostgreSQL、MongoDB
- ✅ 消息队列 - Kafka、RabbitMQ、RocketMQ
- ✅ 容器编排 - Docker Compose、网络
- ✅ 高级用法 - 等待策略、日志、挂载
- ✅ 最佳实践 - 复用、清理、CI/CD
Testcontainers 提供真实的集成测试环境。