← Kursa Dön
📄 Text · 28 min

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?

SorunH2 ileTestcontainers ile
JSON/JSONB sorgularDesteklemezGerçek PostgreSQL çalışır
Full-text searchFarklı davranışProduction ile aynı
Stored proceduresÇalışmazTam uyum
Extension'lar (PostGIS vb.)YokDocker'da mevcut
Production parityDüşü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:

  1. `@Testcontainers`: JUnit 5 extension'ıdır — @Container ile işaretlenen alanları otomatik başlatır/durdurur

  2. `@Container static`: static olduğu için tüm testler boyunca tek container çalışır (test sınıfı başına bir container)

  3. `PostgreSQLContainer`: Docker Hub'dan postgres:16-alpine image'ını çeker ve çalıştırır

  4. `@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

  1. `static` container kullanın: Test sınıfı başına tek container — her test metodu için yeniden başlatma yapılmaz

  2. Alpine image'lar tercih edin: postgres:16-alpine daha hızlı indirilir

  3. Singleton base class: Ortak container'ları abstract sınıfta paylaşın

  4. CI/CD'de Docker: Testcontainers Docker daemon'a erişim gerektirir — CI/CD pipeline'ınızda Docker-in-Docker veya Testcontainers Cloud kullanın

  5. `@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.