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. 參考資源