Testcontainers 導入指引

導入效應

效應 說明
真實環境測試 取代 H2 / HSQLDB,直接用真實的 PostgreSQL / MySQL / Redis 測試
消除環境差異 「在我電腦上可以跑」的問題徹底解決
Integration Test 品質 確保程式碼在真實環境行為正確
AI 產碼驗證 AI 產生的 JPA / SQL 程式碼,用真實 DB 驗證是否正確
容器化測試 支援幾乎所有有 Docker Image 的服務(DB / MQ / Cache)
隔離性 每個測試用獨立的容器,互不干擾

1. Maven 導入

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

<!-- PostgreSQL -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.20.4</version>
    <scope>test</scope>
</dependency>

2. Gradle 導入

testImplementation 'org.testcontainers:testcontainers:1.20.4'
testImplementation 'org.testcontainers:junit-jupiter:1.20.4'
testImplementation 'org.testcontainers:postgresql:1.20.4'

3. 基本使用

3.1 PostgreSQL

@Testcontainers
class UserRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configure(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 shouldSaveUser() {
        // 使用真實 PostgreSQL 測試
        User user = new User("john", "john@example.com");
        repository.save(user);

        Optional<User> found = repository.findByUsername("john");
        assertTrue(found.isPresent());
    }
}

3.2 Redis

@Testcontainers
class CacheServiceTest {

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

    @DynamicPropertySource
    static void configure(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", redis::getFirstMappedPort);
    }

    @Test
    void shouldCacheValue() {
        cacheService.set("key1", "value1", Duration.ofMinutes(5));
        assertEquals("value1", cacheService.get("key1"));
    }
}

3.3 Kafka

@Testcontainers
class KafkaProducerTest {

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

    @DynamicPropertySource
    static void configure(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Test
    void shouldSendMessage() {
        // 測試 Kafka 訊息發送
        producer.send("test-topic", "key", "value");
    }
}

4. 測試策略

Unit Test (Mock/Stub)
    ↓
Integration Test (Testcontainers)
    ↓ E2E Test (真實環境)

4.1 分層測試

層級 工具 目標
Unit JUnit + Mockito 業務邏輯
Integration Testcontainers 資料庫/API 整合
E2E Docker Compose 完整系統流程

5. CI 整合

5.1 Azure Pipeline

- stage: IntegrationTest
  jobs:
    - job: Testcontainers
      steps:
        - task: DockerInstaller@0
          displayName: 'Install Docker'
        - task: Maven@4
          inputs:
            goals: 'test'
            options: '-Dtest=*IntegrationTest'
          displayName: 'Run Integration Tests'

5.2 GitHub Actions

jobs:
  integration-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run Integration Tests
        run: mvn test -Dtest=*IntegrationTest

6. 支援的容器

容器 Module Artifact
PostgreSQL testcontainers-postgresql postgresql
MySQL testcontainers-mysql mysql
MariaDB testcontainers-mariadb mariadb
Redis testcontainers-redis redis
Kafka testcontainers-kafka kafka
MongoDB testcontainers-mongodb mongodb
Elasticsearch testcontainers-elasticsearch elasticsearch
RabbitMQ testcontainers-rabbitmq rabbitmq
Azure SQL testcontainers-azure azure-sql

7. 效能優化

7.1 重複使用容器

@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SharedContainerTest {

    // 所有測試共用一個容器
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    // ...
}

7.2 等待策略

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
    .withStartupTimeout(Duration.ofSeconds(60))
    .waitingFor(Wait.forLogMessage(".*database system is ready.*", 1));

8. 與 H2 比較

特性 H2 Testcontainers
真實 SQL 語法
真實 Index 行為
真實 Lock 行為
啟動速度 極快 需要數秒
Docker 依賴 需要
適用層級 Unit Test Integration Test

9. 參考資源