İçeriğe geç

Testcontainers — Gerçek Veritabanı, Tek Konteyner, Kilitli Kapı

Orta 9 dk Sık karşılaşılır

Önce şunu oku: Spring Test Dilimleri ve Context Cache

30 saniyede özet

Testcontainers testlere gerçek bir PostgreSQL verir. Konteyneri nerede başlattığın, kaç kez başlayacağını ve Spring'in context cache'iyle anlaşıp anlaşamayacağını belirler. Yanlış yerde başlatırsan testler eski porta bağlanmaya çalışır.

Testlerin bilgisayarında hep yeşil. Sorgu canlıda ilk gün patlıyor, çünkü testler başka bir veritabanında koşuyordu. Bu derste testlere gerçeğini vereceğiz ve bunu yaparken düşülen küçük bir çukuru göreceğiz.

  1. Bayt: Testlerde gerçek PostgreSQL kullanıyorum artık. Konteyneri ortak bir taban sınıfa koydum.

  2. Sen: Süper. Hepsi geçiyor mu?

  3. Bayt: İlk sınıf geçiyor. Gerisi 'Connection refused' diyor!

  4. Bayt: Veritabanı ayakta. O zaman testler yanlış kapıyı çalıyor olmalı.

Neden gerçek veritabanı?

H2 gibi bellekteki bir veritabanı hızlıdır ama PostgreSQL değildir. ON CONFLICT, jsonb ya da kilitleme davranışı farklıdır. Testte geçen sorgu canlıda patlayabilir.

TestcontainersTest sırasında Docker'da gerçek bağımlılıklar (PostgreSQL, Kafka, Redis) başlatan kütüphane. Testler canlıdaki yazılımın aynısıyla koşar.Sözlükte gör → test sırasında Docker’da gerçek bir PostgreSQL başlatır. Testler, canlıdaki veritabanının aynı sürümüyle konuşur.

Kafam karıştı, daha basit anlat

Prova, gerçek sahnede yapılırsa sürpriz azalır. Testcontainers testlere gerçek sahneyi kurar.

Hızlı kontrolBaşlangıç

Testlerde H2 yerine Testcontainers ile PostgreSQL kullanmanın asıl kazancı nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

INSERT … ON CONFLICT kullanan bir sorgu H2'de test ediliyor. Ne beklemelisin?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

İki ayrı hafıza

Spring, kurduğu uygulama context’ini bir context cacheSpring'in test bağlamlarını yapılandırmaya göre önbelleğe alması. Anahtar anotasyonlar, property'ler, profiller ve mock'lardır — bir `@MockBean` farkı yeni bir bağlam demektir.Sözlükte gör →’te saklar. Aynı ayarlarla çalışan bir sonraki test sınıfı yeni context kurmaz, cache’tekini kullanır.

Testcontainers’ın JUnit eklentisi ise @Container ile işaretli statik bir konteyneri her test sınıfının başında başlatır, sonunda durdurur. İki sistem de kendi işini doğru yapıyor, ama birbirinden habersiz.

Konteyner ortak bir taban sınıfta, static ve @Container ile işaretli. Bağlantı adresi taban sınıftaki @DynamicPropertySource ile veriliyor. İkinci test sınıfı çalışınca ne olur? Cevabı göster

Connection refused, çünkü JUnit konteyneri ilk sınıftan sonra durdurdu ve yeni bir portta yeniden başlattı. Spring ise aynı ayarları görüp cache’teki context’i verdi. O context hâlâ eski portu tutuyor.

Mutfak taşındı, çizelge taşınmadı.
Adım adım oku
  1. İlk grup oda 101'deki mutfakta: testler geçti.
  2. JUnit konteyneri durdurdu ve yenisini başka bir portta başlattı.
  3. Spring cache'teki context'i verdi; o context hâlâ eski adresi gösteriyor.
  4. Çözüm: konteyneri bir kez başlat ve bütün sınıflar onu kullansın.
Kafam karıştı, daha basit anlat

Spring çizelgeyi bir kez yazıp saklıyor, JUnit mutfağı her seferinde başka odaya taşıyor. İkisi anlaşamayınca kapı kilitli kalıyor.

Hızlı kontrolOrta

Spring'in test context cache'i bir context'i neye göre yeniden kullanır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Taban sınıfta @Testcontainers ve static @Container var. İki alt sınıf aynı context'i paylaşıyor. İkinci sınıf neden 'Connection refused' alır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Bir kez başlat

İlk yol: konteyneri taban sınıfta @Container olmadan, statik bir blokta bir kez start() ile başlatırsın. JVM kapanınca Testcontainers’ın temizlik konteyneri onu kaldırır.

İkinci yol, Spring Boot 3.1 ile geldi: konteyneri ortak bir test ayarında bean olarak tanımlar, üstüne @ServiceConnectionSpring Boot 3.1 ile gelen işaret: bir konteyner bean'inin adresini ve kimlik bilgilerini bağlantı ayarlarına kendiliğinden yazar.Sözlükte gör → yazarsın. Bağlantı ayarları kendiliğinden verilir ve konteyner, context yaşadıkça yaşar.

Kafam karıştı, daha basit anlat

Mutfağı bir kez aç ve her grubu aynı odaya gönder. Ya elle açarsın ya da mutfağı okulun kendisine emanet edersin.

Hızlı kontrolOrta

Bütün entegrasyon testleri için tek bir konteyner ve tek bir context istiyorsun. Hangi yol doğru?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Dört test sınıfı, kaç konteyner?

Tohum 147665
  1. AccountRepositoryITbekliyor
  2. TransferServiceITbekliyor
  3. StatementQueryITbekliyor
  4. LedgerMigrationITbekliyor
Hız
Adım 0

Şu an ne oldu?

Her sınıf kendi konteyneri

Dört test sınıfı aynı PostgreSQL'e ihtiyaç duyuyor. Soru: kaç kez başlatılacak ve Spring context'i kaç kez kurulacak?

Görevler0/3

  • Bir test sınıfını "Connection refused" ile düşüraçık

    İpucu

    Konteyneri JUnit yönetsin, context'i Spring cache'lesin.

  • Dört konteyner ve dört context başlataçık

    İpucu

    Herkes kendi işini kendisi yapsın.

  • Dört sınıfı tek konteyner ve tek context ile, hatasız çalıştıraçık

    İpucu

    İki yolu var.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. Her sınıf kendi konteynerini ve kendi context’ini kurdu: dört ve dört.
  2. “Ortak taban sınıf, @Container” seç. İlk sınıf geçti, diğerleri eski porta çarptı.
  3. “Statik blokta start()” seç. Bir konteyner, bir context, hepsi geçti.
  4. “@ServiceConnection bean” seç. Aynı sonuç, daha az kod.
Hızlı kontrolOrta

Her test sınıfı kendi static @Container'ını ve kendi @DynamicPropertySource metodunu tanımlıyor. Dört sınıf için ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Paylaşılan veri. Bütün sınıflar aynı veritabanını kullanır. Bir testin bıraktığı satır diğerini şaşırtır; her test kendi verisini kursun ya da transaction içinde geri alınsın.

latest etiketi. postgres:latest bir gün sessizce yeni bir ana sürüme geçer. Canlıdaki sürümü sabit yaz: postgres:16-alpine gibi.

Docker’sız makine. CI’da Docker yoksa testler başlamaz. Bu bir test hatası değil, ortam eksiği; CI’ı buna göre kur.

Kafam karıştı, daha basit anlat

Ortak mutfakta herkes kendi tabağını getirir ve yıkar. Fırının modelini de sabit tut.

Hızlı kontrolOrta

Bütün testler tek PostgreSQL konteynerini paylaşıyor. Ara sıra bir test, başka bir testin eklediği satırı görüp düşüyor. En doğru düzeltme nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aşağıdaki örnek bir bankanın hesap servisinden: bütün entegrasyon testleri tek PostgreSQL konteynerini ve tek Spring context’ini paylaşıyor.

Derinleş · Hesap servisi: tek konteyner, tek context 5 dosya · ~66 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/test/java/com/bank/support/ PostgresTestConfig.java Paylaşılan test ayarı: konteyner bir bean, bağlantıyı @ServiceConnection veriyor.

src/test/java/com/bank/support/PostgresTestConfig.java
@TestConfiguration(proxyBeanMethods = false)
public class PostgresTestConfig {
// Same major version as production. Never "latest".
@Bean
@ServiceConnection // Boot fills spring.datasource.* from the running container
PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>("postgres:16-alpine");
}
}

src/test/java/com/bank/account/ AccountRepositoryIT.java Testler aynı anotasyonları ve aynı ayarı kullanıyor: aynı cache anahtarı. @DataJpaTest ile @SpringBootTest ayrı context'ler olurdu ve bean konteyner ikisinde ayrı ayrı başlardı.

src/test/java/com/bank/account/AccountRepositoryIT.java
@SpringBootTest
@Import(PostgresTestConfig.class) // same annotations + same import everywhere -> same cached context
@Transactional
class AccountRepositoryIT {
@Autowired AccountRepository accounts;
@Test
void upsertKeepsOneRowPerIban() {
accounts.upsertBalance("TR330006100519786457841326", new BigDecimal("10.00"));
accounts.upsertBalance("TR330006100519786457841326", new BigDecimal("25.00"));
assertThat(accounts.count()).isEqualTo(1); // ON CONFLICT: a Postgres feature H2 lacks
}
}

src/test/java/com/bank/transfer/ TransferServiceIT.java Her test kendi verisini kuruyor; transaction sonunda geri alınıyor.

src/test/java/com/bank/transfer/TransferServiceIT.java
@SpringBootTest
@Import(PostgresTestConfig.class)
@Transactional // every test rolls back: the shared database stays clean
class TransferServiceIT {
@Autowired TransferService transfers;
@Autowired AccountRepository accounts;
@Test
void movesMoneyBetweenTwoAccounts() {
long from = accounts.save(Account.open("TR01", new BigDecimal("100.00"))).getId();
long to = accounts.save(Account.open("TR02", BigDecimal.ZERO)).getId();
transfers.transfer(from, to, new BigDecimal("40.00"));
assertThat(accounts.findById(to).orElseThrow().balance()).isEqualByComparingTo("40.00");
}
}

src/test/java/com/bank/ LocalAccountApp.java Yerelde geliştirme: aynı ayarla uygulamayı Docker'daki veritabanıyla başlatmak.

src/test/java/com/bank/LocalAccountApp.java
// Run the real app against a throwaway Postgres: ./gradlew bootTestRun
public class LocalAccountApp {
public static void main(String[] args) {
SpringApplication.from(AccountApplication::main)
.with(PostgresTestConfig.class)
.run(args);
}
}

src/test/java/com/bank/support/ AvoidThisBase.java Kaçınılan desen: taban sınıfta @Container ile yönetilen statik konteyner.

src/test/java/com/bank/support/AvoidThisBase.java
// The pattern from the sim that fails: JUnit stops this container after every
// subclass and starts a new one on a new port, while Spring hands the next
// class the cached context that still points at the old port.
@Testcontainers
abstract class AvoidThisBase {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
}

Kendini sına

Şimşek turu1/4

H2'de geçen bir sorgu PostgreSQL'de de kesin geçer.

Soru 1/3İleri

AccountRepositoryIT @DataJpaTest, TransferServiceIT @SpringBootTest; ikisi de @ServiceConnection bean'li aynı ayarı içe aktarıyor. Kaç konteyner başlar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Testcontainers testlere üretimdeki veritabanının aynısını verir. H2'de geçip PostgreSQL'de patlayan sorgular böylece testte yakalanır.
  2. 2 Spring context'i cache'ler, ama konteynerin hâlâ çalışıp çalışmadığını bilmez. JUnit'in her sınıfta durdurup yeniden başlattığı bir konteyner, cache'teki context'i eski bir porta bağlı bırakır.
  3. 3 Paylaşılan konteyner ya bir kez elle başlatılır ya da @ServiceConnection ile bir bean yapılır. Böylece bütün sınıflar tek konteyner ve tek context kullanır.
Sonraki kapı Yeni sürüm hatalı çıktı. Bunu kaç kullanıcı fark etti: hepsi mi, yarısı mı, yoksa yalnızca birkaçı mı? Yayın Stratejileri — Hatalı Sürüm Kaç Kişiye Dokunur? · 9 dk

4 kart sonraki derste seni bekliyor

0/4 kart bu dersten toplandı