Testcontainers
H2 gibi embedded veritabanları hızlıdır ama production'da PostgreSQL veya MySQL kullanıyorsanız, SQL dialect farkları sorunlara yol açabilir. Testcontainers, testlerinizde gerçek Docker container'ları çalıştırmanızı sağlayan bir Java kütüphanesidir. PostgreSQL, MySQL, Redis, Kafka, Elasticsearch — herhangi bir Docker image'ı test ortamında kullanabilirsiniz.
Neden Testcontainers?
| Sorun | H2 ile | Testcontainers ile |
|---|---|---|
| JSON/JSONB sorgular | Desteklemez | Gerçek PostgreSQL çalışır |
| Full-text search | Farklı davranış | Production ile aynı |
| Stored procedures | Çalışmaz | Tam uyum |
| Extension'lar (PostGIS vb.) | Yok | Docker'da mevcut |
| Production parity | Düşük | Çok yüksek |
Bağımlılık Kurulumu
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Veritabanı modülü (ihtiyaca göre) -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>Spring Boot 3.1+ sürümünde BOM (Bill of Materials) yönetimi otomatiktir; versiyon belirtmeniz gerekmez.
PostgreSQL Container
@SpringBootTest
@Testcontainers // JUnit 5 extension'ını aktifleştirir
class UserRepositoryContainerTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@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);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndRetrieveUser() {
User user = User.builder()
.name("Ali")
.email("ali@test.com")
.password("encoded")
.build();
User saved = userRepository.save(user);
assertNotNull(saved.getId());
Optional<User> found = userRepository.findByEmail("ali@test.com");
assertTrue(found.isPresent());
}
}Adım adım açıklayalım:
`@Testcontainers`: JUnit 5 extension'ıdır —
@Containerile işaretlenen alanları otomatik başlatır/durdurur`@Container static`:
staticolduğu için tüm testler boyunca tek container çalışır (test sınıfı başına bir container)`PostgreSQLContainer`: Docker Hub'dan
postgres:16-alpineimage'ını çeker ve çalıştırır`@DynamicPropertySource`: Container'ın rastgele atanan port'unu ve bağlantı bilgilerini Spring'in property'lerine enjekte eder
MySQL Container
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass")
.withInitScript("schema.sql"); // Container başlatıldığında çalıştırılır
@DynamicPropertySource
static void mysqlProperties(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",
() -> "com.mysql.cj.jdbc.Driver");
}GenericContainer — Herhangi Bir Docker Image
Redis, Kafka, Elasticsearch gibi özel container'lar için:
@Container
static GenericContainer<?> redis = new GenericContainer<>(
DockerImageName.parse("redis:7-alpine"))
.withExposedPorts(6379);
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port",
() -> redis.getMappedPort(6379));
}
// Kafka Container
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));
@DynamicPropertySource
static void kafkaProperties(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers",
kafka::getBootstrapServers);
}Container'ları Paylaşma (Singleton Pattern)
Her test sınıfı için yeni container başlatmak yavaştır. Container'ı paylaşmak:
// Abstract base class — tüm container testleri bunu extend eder
@SpringBootTest
@Testcontainers
abstract class AbstractContainerTest {
@Container
static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@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);
}
}
// Alt sınıflar aynı container'ı paylaşır
class UserRepoTest extends AbstractContainerTest {
@Test void testSaveUser() { /* ... */ }
}
class ProductRepoTest extends AbstractContainerTest {
@Test void testSaveProduct() { /* ... */ }
}Spring Boot 3.1+ — @ServiceConnection
Spring Boot 3.1 ile gelen @ServiceConnection anotasyonu, @DynamicPropertySource yerine kullanılabilir — daha az boilerplate:
@SpringBootTest
@Testcontainers
class ModernContainerTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
// @DynamicPropertySource GEREK YOK!
// Spring Boot otomatik olarak bağlantı bilgilerini algılar
@Autowired
private UserRepository userRepository;
@Test
void shouldWorkWithAutoConfig() {
User saved = userRepository.save(new User("Ali", "ali@test.com"));
assertNotNull(saved.getId());
}
}Wait Strategies ve Health Check
Container'ın tamamen hazır olmasını beklemek:
@Container
static GenericContainer<?> customApp = new GenericContainer<>("my-app:latest")
.withExposedPorts(8080)
.waitingFor(Wait.forHttp("/actuator/health")
.forPort(8080)
.forStatusCode(200)
.withStartupTimeout(Duration.ofSeconds(60)));Best Practices
`static` container kullanın: Test sınıfı başına tek container — her test metodu için yeniden başlatma yapılmaz
Alpine image'lar tercih edin:
postgres:16-alpinedaha hızlı indirilirSingleton base class: Ortak container'ları abstract sınıfta paylaşın
CI/CD'de Docker: Testcontainers Docker daemon'a erişim gerektirir — CI/CD pipeline'ınızda Docker-in-Docker veya Testcontainers Cloud kullanın
`@ServiceConnection`: Spring Boot 3.1+ kullanıyorsanız bu anotasyonu tercih edin
Testcontainers, "it works on my machine" problemini test seviyesinde çözer. Production veritabanınızla birebir aynı ortamda test çalıştırarak, deploy öncesi güvenilirliğinizi maksimuma çıkarırsınız.
AI Asistan
Sorularını yanıtlamaya hazır