• 17.08.2026 09:03:10
  • Admin Admin

Spring Boot uygulamalarında PostgreSQL, Kafka ve kimlik doğrulama bileşenlerini Testcontainers ile gerçek bağımlılıklarla test edin; spring rest api sözleşmelerindeki üretimden kaçan farkları erken yakalayın.

Spring Boot’ta Testcontainers ile Gerçekçi Entegrasyon Testleri

Spring Boot eğitimi perspektifiyle: Neden in-memory test yetmez?

Bir spring rest api için H2 tabanlı testin yeşil olması, PostgreSQL üretim davranışını doğrulamaz. Örneğin PostgreSQL’de jsonb, case-sensitive quoted identifier, dizi tipleri ve transaction isolation semantiği H2 ile aynı değildir. spring boot eğitimi kapsamında sık görülen hata, repository testini H2 ile geçip PostgreSQL’de operator does not exist: character varying = uuid veya yanlış sıralama davranışıyla karşılaşmaktır. Testcontainers bağımlılığını test kapsamına ekleyip gerçek veritabanını her CI çalıştırmasında başlatın:

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

Bu yaklaşımın mekanizması şudur: uygulamanın JDBC sürücüsü TCP üzerinden container içindeki gerçek PostgreSQL sunucusuna bağlanır; dolayısıyla SQL parser, sorgu planlayıcı ve tip dönüşümleri üretimdeki bileşen tarafından yürütülür. Yerel Docker erişimini doğrulamak için geliştirici makinesinde ./mvnw test -Dtest=ContextSmokeTest, CI ajanında ise Docker socket veya rootless Docker erişimiyle aynı komutu çalıştırın. Image pull süresini test süresinden ayırmak için CI cache politikanızda PostgreSQL image katmanlarını saklayın; aksi halde ilk koşum ile sıcak koşum sürelerini aynı metrikte yorumlamak yanıltıcı olur.

Mevcut bir java spring eğitimi veya spring boot kursu projesinde ilk hedef, tüm testleri container’a taşımak değil; üretim hatası maliyeti yüksek repository ve migration testlerini seçmektir. Örneğin Flyway migration’larının gerçek veritabanında uygulanabildiğini şu komutla ayrı bir build adımında görünür kılın: ./mvnw -Dtest=FlywayMigrationIT verify. Migration testi, yalnızca uygulama context’inin açıldığını değil, eski şemadan yeni şemaya yükseltmenin de çalıştığını kanıtlamalıdır.

Spring Framework test context ile PostgreSQL bağlantısını dinamik bağlamak

Sabit spring.datasource.url yazmayın; Testcontainers her başlatmada rastgele host portu atar. Spring TestContext’in container yaşam döngüsünü yönetebilmesi için @ServiceConnection kullanın. Bu anotasyon, container türünden JDBC URL, kullanıcı adı ve parolayı çıkarıp uygulama context’ine ConnectionDetails olarak verir; test profiline port kopyalama ihtiyacını ortadan kaldırır:

@SpringBootTest
@Testcontainers
class OrderRepositoryIT {

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

  @Autowired OrderRepository orderRepository;

  @Test
  void findsOrdersByTenant() {
    var order = orderRepository.save(new Order("tenant-a", "OPEN"));
    assertThat(orderRepository.findByTenantId("tenant-a"))
        .extracting(Order::getId)
        .containsExactly(order.getId());
  }
}

@ServiceConnection kullanılamayan özel bir bağımlılıkta @DynamicPropertySource doğru kaçış kapısıdır. Önemli incelik: property kaydı container başladıktan sonra endpoint okumalıdır; getMappedPort() çağrısını statik initializer içinde yapmak container henüz başlamadıysa hata üretir. Örneğin Redis için supplier ile gecikmeli kayıt yapın:

@Container
static GenericContainer<?> redis = new GenericContainer<>("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));
}

Test isolation için sadece @Transactional rollback’ine güvenmeyin. Uygulama kodu REQUIRES_NEW ile transaction açıyorsa, mesaj tüketicisi farklı thread’de yazıyorsa veya HTTP çağrısı sonrası veri oluşuyorsa test transaction’ı bu kayıtları geri alamaz. Her testten önce hedef tabloları TRUNCATE TABLE orders, outbox_event RESTART IDENTITY CASCADE ile temizleyen bir @BeforeEach yardımcı metodu yazın ya da her test sınıfına ayrı şema oluşturun. Bu ayrım, paralel Maven testlerinde başka sınıfın verisini yanlışlıkla assert etmeyi engeller.

Microservices mimarisi ve Spring Cloud bağımlılıklarını sınırda test etmek

microservices mimarisi içinde her servisi tek JVM’de mock’lamak, ağ üzerinden taşınan header, timeout ve serialization farklarını gizler. Ancak tüm sistemi her PR’da ayağa kaldırmak da yavaş ve kırılgandır. Dengeli sınır şudur: Servisin sahip olduğu PostgreSQL ve Kafka gibi stateful altyapıyı Testcontainers ile gerçek çalıştırın; sahip olmadığınız HTTP servisleri için WireMock kullanın. spring cloud ile çalışan bir istemcinin hata eşlemesini WireMock senaryosunda doğrulayın: 503 döndüğünde domain exception üretmeli, 200 döndüğünde ise response DTO’nun alanları kaybolmamalıdır.

Kafka testi için embedded broker yerine gerçek protocol ve broker davranışı sağlayan KafkaContainer kullanın. Aşağıdaki yapı, Spring Boot’un Kafka bağlantısını container’dan alır ve testin tüketicinin gerçekten mesaj işlediği ana kadar beklemesini sağlar:

@SpringBootTest
@Testcontainers
class PaymentEventIT {

  @Container
  @ServiceConnection
  static KafkaContainer kafka = new KafkaContainer("apache/kafka:3.8.0");

  @Autowired KafkaTemplate<String, PaymentCreated> kafkaTemplate;
  @Autowired PaymentProjectionRepository projections;

  @Test
  void consumesPaymentCreated() {
    kafkaTemplate.send("payment.created", "p-42",
        new PaymentCreated("p-42", new BigDecimal("19.90"))).join();

    await().atMost(Duration.ofSeconds(10)).untilAsserted(() ->
        assertThat(projections.findByPaymentId("p-42")).isPresent());
  }
}

Buradaki kritik ayrıntı, KafkaTemplate.send(...).join() çağrısının yalnızca broker ack aldığını, tüketicinin işleyiciyi tamamladığını göstermemesidir. Bu nedenle Awaitility ile gözlemlenebilir iş sonucu üzerinde bekleyin. Ayrıca test consumer group kimliğini her sınıf için benzersiz üretin; sabit group id kullanılırsa önceki testin committed offset’i yeni testte mesajın atlanmasına neden olur. Örnek property: spring.kafka.consumer.group-id=payment-it-${random.uuid}.

Spring Security ve Spring MVC katmanını gerçek kimlik akışıyla doğrulamak

spring mvc controller testinde yalnızca @WithMockUser kullanmak yetki kuralını doğrular, fakat JWT decoder, issuer doğrulaması ve claim-to-authority dönüşümünü devre dışı bırakır. spring security resource server kullanan uygulamada en az bir entegrasyon testini gerçek bir OpenID Connect sağlayıcısıyla çalıştırın. Keycloak Testcontainers modülüyle realm import edilerek test token’ı alınabilir; bu, yanlış issuer-uri, eksik audience ve hatalı role claim mapper sorunlarını production öncesinde yakalar.

HTTP sözleşmesi için WebTestClient ile status, content type ve problem body alanlarını birlikte assert edin. Aşağıdaki örnekte token, test yardımcı istemcisi tarafından alınmış gerçek access token’dır; sadece 403 kontrolü yerine uygulamanın yetki kararının hangi claim üzerinden verildiği kanıtlanır:

@Autowired WebTestClient client;

@Test
void rejectsInvoiceReadWithoutScope() {
  String token = keycloakClient.tokenFor("billing-writer");

  client.get().uri("/api/invoices/INV-7")
      .headers(h -> h.setBearerAuth(token))
      .exchange()
      .expectStatus().isForbidden()
      .expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)
      .expectBody()
      .jsonPath("$.status").isEqualTo(403)
      .jsonPath("$.detail").value(containsString("invoice:read"));
}

Yaygın edge case: uygulama testte localhost üzerinden HTTP çağrısı yaparken cookie tabanlı akışta Secure cookie gönderilmez; production HTTPS davranışını bu testten çıkaramazsınız. Bearer token kullanan API’lerde bu sorun yoktur. Session/cookie akışı test ediliyorsa TLS terminate eden bir reverse proxy container ekleyin veya HTTP cookie davranışını ayrı bir browser-level testte ölçün. Ayrıca spring-security-test ile yapılan hızlı controller testlerini koruyun; gerçek IdP testi az sayıda kritik authorization yolunda, daha pahalı bir entegrasyon katmanı olarak konumlanmalıdır.

Test süresini JFR ile ölçmek: önce-sonra optimizasyonu

Container reuse veya paralel test kararı sezgiyle verilmemeli; önce Java Flight Recorder ile test JVM’inin zamanını ölçün. Maven Surefire için önce referans ölçümü alın: ./mvnw -Dtest='*IT' -DargLine='-XX:StartFlightRecording=filename=target/it-before.jfr,dumponexit=true,settings=profile' test. JDK Mission Control’da Method Profiling, Socket Read ve Java Application görünümlerini inceleyin. Container başlangıcı baskınsa Docker image pull/başlatma süresi; context bootstrap baskınsa bean oluşturma ve classpath taraması; test gövdesi baskınsa SQL veya ağ beklemeleri ayrı ayrı görünür.

Örneğin aynı PostgreSQL container’ını test sınıfları arasında yeniden başlatmak yerine statik container ve Spring context cache kullanmak ölçülebilir bir değişikliktir. Ancak bunun güvenli olması için önceki bölümdeki tablo temizliği zorunludur. Surefire fork sayısını da CPU ve Docker daemon kapasitesine göre sınırlandırın:

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <forkCount>1C</forkCount>
    <reuseForks>true</reuseForks>
  </configuration>
</plugin>

Değişiklikten sonra aynı makine sınıfında aynı test seçicisiyle ikinci kayıt alın: ./mvnw -Dtest='*IT' -DargLine='-XX:StartFlightRecording=filename=target/it-after.jfr,dumponexit=true,settings=profile' test. Toplam duvar saati yanında JFR’deki socket read süresi, Spring context sayısı ve container startup sayısını karşılaştırın. Örneğin süre düşerken flaky test oranı artıyorsa paralel testler ortak Kafka topic’i veya ortak tabloyu kirletiyor olabilir; bu durumda paralelliği geri almak yerine topic adlarına ${random.uuid} eklemek ve veri temizliğini deterministik hale getirmek doğru düzeltmedir. Bu ölçüm disiplini, bir spring framework eğitimi içinde test hızını sadece "hızlı" diye değil, darboğazın nerede olduğuna göre iyileştirmeyi öğretir.

Sık Sorulan Sorular

Spring Boot eğitimi için Testcontainers mı H2 mi kullanılmalı?

Hızlı unit testlerde H2 kullanılabilir; ancak PostgreSQL’e özgü SQL, Flyway migration, jsonb, indeks ve transaction davranışı olan repository testlerinde Testcontainers kullanın. Aynı repository testini PostgreSQL container’ında çalıştırıp üretim sürücüsüyle SQL hatalarını doğrulayın.

Spring Security ile gerçek JWT entegrasyon testi nasıl yazılır?

Keycloak gibi bir OIDC sağlayıcısını Testcontainers ile başlatın, test realm’ini import edin, test kullanıcısı için access token alın ve WebTestClient çağrısında setBearerAuth kullanın. En az issuer, audience ve role-to-authority dönüşümünü kapsayan bir 200 ve bir 403 senaryosu ekleyin.

Spring Cloud microservices mimarisi testlerinde KafkaContainer ne zaman gerekir?

Mesaj serialization, consumer group offset, retry veya header aktarımı servis davranışını etkiliyorsa KafkaContainer kullanın. send().join() sonrasında doğrudan assert etmeyin; Awaitility ile veritabanı projection’ı veya gözlemlenebilir başka bir tüketim sonucunu en fazla belirlenmiş süre içinde bekleyin.

Spring MVC ve spring rest api entegrasyon testleri neden yavaşlar?

JFR ile container başlangıcı, Spring context bootstrap ve socket wait zamanlarını ayırın. Container başlangıcı baskınsa statik container ve context cache deneyin; test verisini TRUNCATE ile temizleyin. Ağ beklemesi baskınsa WireMock gecikmelerini, Kafka tüketim timeout’larını ve gereksiz tekrar denemelerini ölçün.

AI / LLM Discovery

Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt