Skip to content

Testcontainers

Testcontainers 是一个开源测试库,通过 Docker 容器为集成测试提供真实的外部服务实例(数据库、消息队列、缓存等),从根本上消除了 Mock 与真实环境之间的差异。它支持 Java、Spring Boot、Go、Node.js、Python 等多种语言,已成为 2026 年集成测试的事实标准。

为什么需要 Testcontainers?

传统集成测试的痛点

方案问题
H2 / 内嵌数据库SQL 方言不同,存储过程、函数、索引行为不一致
Mock 外部服务无法验证真实交互,Mock 逻辑本身可能出错
共享测试环境测试相互干扰,数据污染,环境不稳定
手动搭建环境新人上手慢,CI 环境配置复杂

Testcontainers 的核心价值

  • 真实环境:使用与生产一致的 Docker 镜像(PostgreSQL、Redis、Kafka 等)
  • 用完即弃:测试结束后自动销毁容器,无残留数据
  • 环境隔离:每次测试获得独立环境,避免相互干扰
  • 零配置:开发者拉取代码即可运行测试,Docker 是唯一前置依赖
  • CI/CD 友好:与 Jenkins、GitHub Actions 等无缝集成

核心架构与工作原理

Testcontainers 通过 Docker API 管理容器生命周期,核心组件如下:

┌──────────────────────────────────────────┐
│              Test JVM                     │
│  ┌────────────────────────────────────┐  │
│  │  @Testcontainers / @Container      │  │
│  │  GenericContainer / Module         │  │
│  └──────────────┬─────────────────────┘  │
│                 │ Docker API              │
│  ┌──────────────▼─────────────────────┐  │
│  │  Docker Daemon                      │  │
│  │  ┌──────────┐  ┌──────────┐        │  │
│  │  │PostgreSQL│  │  Redis   │        │  │
│  │  │ Container│  │ Container│  ...   │  │
│  │  └──────────┘  └──────────┘        │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘

关键组件

组件说明
GenericContainer通用容器,可运行任意 Docker 镜像
Module(模块)针对特定服务的封装(如 PostgreSQLContainer),内置默认配置和等待策略
ryuk 容器Testcontainers 的守护容器,负责在 JVM 异常退出时清理残余容器
Wait Strategy等待策略,确保容器内的服务就绪后再开始测试

生命周期

java
// 经典 JUnit 5 模式
@BeforeAll  → 容器启动(静态字段,所有测试方法共享)
  @BeforeEach → 每个测试方法前的准备工作
    @Test → 执行测试
  @AfterEach → 清理测试数据
@AfterAll → 容器停止并销毁

快速入门

环境要求

  • Java 8+
  • Docker(本地或远程 Docker Daemon)

添加依赖

Maven

xml
<!-- Testcontainers BOM(统一版本管理) -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-bom</artifactId>
            <version>1.20.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 核心库 -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>

<!-- JUnit 5 扩展 -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

<!-- PostgreSQL 模块 -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>

Gradle

groovy
testImplementation platform('org.testcontainers:testcontainers-bom:1.20.1')
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'

第一个集成测试

java
import org.junit.jupiter.api.*;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.sql.*;

@Testcontainers
class CustomerRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Test
    void shouldInsertAndQueryCustomer() throws SQLException {
        // 获取动态连接信息
        String url = postgres.getJdbcUrl();
        String user = postgres.getUsername();
        String pass = postgres.getPassword();

        try (Connection conn = DriverManager.getConnection(url, user, pass)) {
            // 建表
            conn.createStatement().execute("""
                CREATE TABLE customers (
                    id SERIAL PRIMARY KEY,
                    name VARCHAR(100) NOT NULL
                )
            """);

            // 插入
            PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO customers (name) VALUES (?)"
            );
            ps.setString(1, "张三");
            ps.executeUpdate();

            // 查询验证
            ResultSet rs = conn.createStatement().executeQuery(
                "SELECT COUNT(*) FROM customers"
            );
            rs.next();
            Assertions.assertEquals(1, rs.getInt(1));
        }
    }
}

运行测试时,Testcontainers 会自动拉取 postgres:16-alpine 镜像、启动容器、执行测试、销毁容器,全程无需手动干预。

常用模块一览

Testcontainers 提供了丰富的预置模块,覆盖主流中间件:

模块依赖 artifact说明
PostgreSQLpostgresql最常用的关系型数据库模块
MySQLmysqlMySQL 8.x 支持
MongoDBmongodbNoSQL 文档数据库
Redis核心库通过 GenericContainer 启动
Kafkakafka消息队列,支持 Confluent 镜像
RabbitMQrabbitmqAMQP 消息中间件
Elasticsearchelasticsearch全文搜索引擎
Cassandracassandra分布式 NoSQL 数据库
MinIO核心库S3 兼容的对象存储
Seleniumselenium浏览器自动化测试
Oracleoracle-xeOracle 数据库(需接受许可)
Toxiproxytoxiproxy网络故障注入,模拟网络异常

ComposeContainer — 多容器编排

当测试依赖多个服务时,可用 DockerComposeContainer 一键启动整套环境:

java
@Container
static DockerComposeContainer<?> environment = new DockerComposeContainer<>(
    new File("src/test/resources/docker-compose.yml")
)
    .withExposedService("postgres", 5432)
    .withExposedService("redis", 6379)
    .withExposedService("kafka", 9092);

Spring Boot 3.x 深度集成

Spring Boot 3.1+ 引入了 @ServiceConnection 注解和 spring-boot-testcontainers 模块,大幅简化集成测试配置。

添加 Spring Boot Testcontainers 依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>

@ServiceConnection — 自动注入连接信息

只需在容器字段上添加 @ServiceConnection,Spring Boot 会自动创建 ConnectionDetails Bean,覆盖 DataSource 等自动配置:

java
@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveAndFindUser() {
        User user = new User("李四", "lisi@example.com");
        userRepository.save(user);

        Optional<User> found = userRepository.findByEmail("lisi@example.com");
        assertThat(found).isPresent();
    }
}

传统方式对比

没有 @ServiceConnection 时,需要手动配置 @DynamicPropertySource

java
@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);
}

@ServiceConnection 正是对上述繁琐配置的语义化封装。

Spring Bean 方式管理容器(推荐)

将容器声明为 Spring Bean,利用 Spring 的生命周期自动管理启停,避免与 @DirtiesContext 等上下文相关问题:

java
@SpringBootTest
class RedisCacheTest {

    @TestConfiguration
    static class TestcontainersConfig {

        @Bean
        @ServiceConnection(name = "redis")
        GenericContainer<?> redisContainer() {
            return new GenericContainer<>("redis:7-alpine")
                .withExposedPorts(6379);
        }
    }

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Test
    void shouldCacheValue() {
        redisTemplate.opsForValue().set("key", "hello");
        assertThat(redisTemplate.opsForValue().get("key")).isEqualTo("hello");
    }
}

@ServiceConnection 支持的服务

Spring Boot 自动识别以下容器的连接信息:

容器/镜像生成的 ConnectionDetails
PostgreSQLContainer / MySQLContainerJdbcConnectionDetails + R2dbcConnectionDetails
MongoDBContainerMongoConnectionDetails
RedisContainer / "redis"RedisConnectionDetails
KafkaContainerKafkaConnectionDetails
RabbitMQContainerRabbitMQConnectionDetails
ElasticsearchContainerElasticsearchConnectionDetails
CassandraContainerCassandraConnectionDetails
JdbcDatabaseContainer 子类FlywayConnectionDetails / LiquibaseConnectionDetails

实践案例

案例一:Spring Boot + PostgreSQL Repository 层测试

这是最典型的应用场景,测试 Spring Data JPA Repository 在真实 PostgreSQL 环境下的表现:

java
@SpringBootTest
@Testcontainers
class OrderRepositoryTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldPersistOrderWithItems() {
        Order order = new Order();
        order.setCustomerName("王五");
        order.addItem(new OrderItem("商品A", 2, new BigDecimal("99.00")));
        order.addItem(new OrderItem("商品B", 1, new BigDecimal("199.00")));

        Order saved = orderRepository.save(order);

        assertThat(saved.getId()).isNotNull();
        assertThat(saved.getItems()).hasSize(2);
        assertThat(saved.getTotalAmount()).isEqualByComparingTo(new BigDecimal("397.00"));
    }

    @Test
    void shouldFindOrdersByCustomer() {
        orderRepository.save(new Order("赵六", List.of()));
        orderRepository.save(new Order("赵六", List.of()));

        List<Order> orders = orderRepository.findByCustomerName("赵六");

        assertThat(orders).hasSize(2);
    }
}

案例二:Redis 缓存集成测试

验证 @Cacheable 注解在真实 Redis 环境中的缓存行为:

java
@SpringBootTest
@Testcontainers
class ProductServiceCacheTest {

    @Container
    @ServiceConnection
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @Autowired
    private ProductService productService;

    @Test
    void shouldCacheProductDetail() {
        // 首次查询:走数据库
        ProductDetail first = productService.getProductDetail("P001");

        // 二次查询:应命中缓存
        ProductDetail second = productService.getProductDetail("P001");

        assertThat(first).isEqualTo(second);
    }
}

案例三:Kafka 消息驱动测试

验证消息生产和消费的完整链路:

java
@SpringBootTest
@Testcontainers
class OrderEventPublisherTest {

    @Container
    @ServiceConnection
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
    );

    @Autowired
    private OrderEventPublisher publisher;

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Test
    void shouldPublishAndConsumeOrderEvent() throws Exception {
        OrderEvent event = new OrderEvent("ORDER-001", "CREATED");

        // 发送消息
        publisher.publish(event);

        // 消费验证
        ConsumerRecord<String, OrderEvent> record =
            KafkaTestUtils.getSingleRecord(consumer, "order-events", Duration.ofSeconds(10));

        assertThat(record.value().getOrderId()).isEqualTo("ORDER-001");
        assertThat(record.value().getType()).isEqualTo("CREATED");
    }
}

案例四:多容器编排集成测试

当微服务依赖多个中间件时,用 ComposeContainer 统一管理:

yaml
# src/test/resources/docker-compose-test.yml
version: '3.8'
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_PASSWORD: test
  redis:
    image: redis:7-alpine
java
@SpringBootTest
class FullStackIntegrationTest {

    @Container
    static DockerComposeContainer<?> env = new DockerComposeContainer<>(
        new File("src/test/resources/docker-compose-test.yml")
    )
        .withExposedService("postgres", 5432)
        .withExposedService("redis", 6379);

    @DynamicPropertySource
    static void overrideProperties(DynamicPropertyRegistry registry) {
        // 从 Compose 容器动态注入配置
        registry.add("spring.datasource.url",
            () -> "jdbc:postgresql://" + env.getServiceHost("postgres", 5432)
                + ":" + env.getServicePort("postgres", 5432) + "/testdb");
        registry.add("spring.data.redis.host",
            () -> env.getServiceHost("redis", 6379));
        registry.add("spring.data.redis.port",
            () -> env.getServicePort("redis", 6379));
    }

    // ... 测试代码
}

性能优化与最佳实践

1. 单例容器复用(最重要)

默认情况每个测试类都会启动独立容器,耗时严重。对于只读或无状态服务,应跨测试类复用容器:

java
// 抽象基类,所有集成测试继承它
public abstract class AbstractIntegrationTest {

    static final PostgreSQLContainer<?> POSTGRES;

    static {
        POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
        POSTGRES.start();
    }
}

// 子类直接复用
class UserRepositoryTest extends AbstractIntegrationTest {
    // 无需再声明 @Container,直接使用 POSTGRES
}

Spring Boot 中推荐用 @TestConfiguration + Bean 方式,Spring 上下文缓存机制会自动复用容器。

2. ryuk 容器与资源回收

Testcontainers 启动时会额外启动一个 testcontainers/ryuk 容器作为看门狗,负责在 JVM 进程(包括异常退出)结束后清理测试容器。可通过环境变量禁用:

bash
# 禁用 ryuk(需自行管理容器生命周期)
export TESTCONTAINERS_RYUK_DISABLED=true

一般情况下不建议禁用,除非在受限环境中运行。

3. CI/CD 中的 Docker-in-Docker

GitHub Actions 示例:

yaml
name: Integration Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run Tests
        run: ./mvnw verify
        # GitHub Actions 默认提供 Docker,无需额外配置

注意

如果 CI Runner 本身在容器内运行(DinD),需要挂载 Docker Socket:

yaml
services:
  docker:
    image: docker:dind
    options: --privileged

4. 并行测试策略

java
// JUnit 5 并行执行配置
// src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

配合单例容器复用,可显著缩短测试总耗时。

5. 镜像预热

在 CI 中提前拉取镜像,避免首次运行时等待:

yaml
- name: Pre-pull Docker images
  run: docker pull postgres:16-alpine

6. 等待策略优化

不要用固定延时(sleep),应使用语义化等待:

java
// ❌ 不推荐:固定等待
new GenericContainer<>("redis:7-alpine")
    .withStartupTimeout(Duration.ofSeconds(30));

// ✅ 推荐:等待特定日志或端口就绪
new GenericContainer<>("redis:7-alpine")
    .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1));

// ✅ 推荐:等待健康检查通过
new GenericContainer<>("postgres:16-alpine")
    .waitingFor(Wait.forHealthcheck());

常见问题

容器启动失败

Could not find a valid Docker environment

解决:确保 Docker Daemon 正在运行,且当前用户有权限访问 Docker Socket。

macOS / Windows 性能

在非 Linux 系统上 Docker 运行在虚拟机中,I/O 性能较差。建议:

  • macOS:使用 --mount type=tmpfs,destination=/var/lib/postgresql/data 将数据目录挂载到内存
  • 全局配置:使用 Testcontainers Desktop 应用

CI 环境中 Socket 权限

bash
# Jenkins / GitLab CI
sudo chmod 666 /var/run/docker.sock

总结

Testcontainers 通过 Docker 容器消除了集成测试中最根本的问题——环境不一致。它的核心优势:

维度效果
正确性测试运行在与生产一致的数据库/中间件上,杜绝方言差异
可重复性每次测试都是全新环境,不依赖外部状态
开发体验一条命令即可运行全部测试,新人零配置上手
CI/CD与主流 CI 平台无缝集成,Docker 是唯一前置条件
Spring Boot@ServiceConnection 让集成测试配置量趋近于零

如果你的项目中还在使用 H2 或 Mock 做集成测试,现在是时候迁移到 Testcontainers 了。


作者:yanshaodong