İçeriğe geç

Spring Test Dilimleri ve Context Cache

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

Önce şunu oku: Test Piramidi, JUnit 5 ve Mockito

30 saniyede özet

Test dilimleri (@WebMvcTest gibi) uygulamanın yalnızca bir parçasını açar. Bütün test süresini sanıldığı kadar düşürmezler, çünkü Spring açtığı uygulamayı zaten testler arasında paylaşır. Asıl yavaşlık farklı ayarların sayısından gelir.

Testler yavaşlayınca akla ilk gelen, uygulamanın tamamı yerine yalnızca küçük bir parçasını açmak olur. Doğru yönde bir fikir — ama neden işe yaradığı çoğu zaman yanlış biliniyor.

  1. Bayt: Test suite'imiz on dakika sürüyor! Hepsi @SpringBootTest, suç onda.

  2. Sen: Hepsini küçük parçalara bölelim o zaman.

  3. Bayt: Böldüm. Toplam süre neredeyse hiç değişmedi...

  4. Bayt: Demek ki yavaşlığın asıl sebebi başka bir yerde.

Spring açtığını saklar

Spring test context’ini test sınıfı başına değil, yapılandırma başına oluşturur. Sekiz sınıf aynı annotation ve aynı ayarları kullanıyorsa context bir kez başlar, yedi kez cache’ten gelir.

Yani “sekiz @SpringBootTest sınıfı = sekiz uygulama başlatma” yanlıştır. Bu tek bilgi, suite süresini nereden düşüreceğini belirler.

Kafam karıştı, daha basit anlat

Spring, aynı ayarlarla kurulan test ortamını bir kez açar ve saklar. Sekiz test sınıfı aynı ayarı kullanıyorsa, uygulama sekiz kez değil bir kez başlar.

Kendin gör

Yalnızca mutfağı aç, açtığını da paylaş.
Adım adım oku
  1. @SpringBootTest uygulamanın bütün katmanlarını açar: web, servis, veritabanı, güvenlik.
  2. @WebMvcTest gibi bir dilim yalnızca test ettiğin katmanı açar; tek sınıfı çalıştırmak çok hızlanır.
  3. Spring açtığı uygulamayı ayara göre saklar; aynı ayardaki test sınıfları tek bir açılışı paylaşır.
  4. @MockitoBean ya da farklı bir profil yeni bir ayar demektir ve yeni bir açılışa yol açar.

Spring test dilimleri — context cache, geri bildirim süresi ve izolasyon

Tohum 4
  • UserControllerTest@SpringBootTest6 test
  • OrderControllerTest@SpringBootTest5 test
  • AuthControllerTest@SpringBootTest4 test
  • UserRepositoryTest@SpringBootTest5 test
  • OrderRepositoryTest@SpringBootTest4 test
  • UserServiceTest@SpringBootTest8 test
  • PricingServiceTest@SpringBootTest6 test
  • CheckoutFlowIT@SpringBootTest3 test

Başlatılan context’ler — 0/1

  • @SpringBootTest

Toplam suite

0.0 sn

12.0 sn başlatma + 16.4 sn test

Tek sınıf (soğuk)

14.4 sn

geliştiricinin günde onlarca kez ödediği süre

En büyük context

214 bean

1 farklı context · 0 cache isabeti

Hız
Adım 0

Şu an ne oldu?

Suite başlıyor

8 sınıf, 41 test. Spring her farklı yapılandırma için ayrı bir context başlatır ve onu suite boyunca önbellekte tutar.

Aklında kalsın: Test context’i test sınıfı başına değil, yapılandırma başına oluşturulur.

Olay günlüğü (0)

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

Sırayla dene ve ortadaki kutuya dikkat et:

  1. Her şey @SpringBootTest. Toplam ~28 sn, tek context. Tek sınıfı çalıştırmak 14.4 sn.
  2. Dilimlere geç. Toplam neredeyse aynı (~28 sn). Ama tek sınıf 2.9 sn — beş kat.
  3. Dilimler + birim testi. Toplam 22 sn’ye düşüyor: servis testleri Spring’e hiç ihtiyaç duymuyor.
  4. @MockitoBean anahtarını aç (1. kurulumda). Context 1’den 2’ye çıkıyor, süre +12 sn.
  5. Testcontainers’ı aç. Veritabanına bağlanan her context ayrı konteyner başlatıyor.
Hızlı kontrolOrta

Spring test bağlamı (application context) testler arasında yeniden kullanılır mı?

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

Dilimler neyi açar?

Bir controller testi @WebMvcTest ile yazıldı. Controller bir TransferService'e ihtiyaç duyuyor. Bu servis context'te kendiliğinden var mıdır? Cevabı göster

Hayır. @WebMvcTest yalnızca web katmanını açar; servisi @MockitoBean ile sen vermelisin.

AnnotationYüklerYüklemez
@WebMvcTestController, @ControllerAdvice, filtreler, Jackson, MockMvcServis, repository, DataSource
@DataJpaTestEntity, repository, DataSource, TestEntityManagerController, servis, web katmanı
@SpringBootTestHer şey—

Dilimin yüklemediği her bean @MockitoBean ile verilmelidir. @WebMvcTest içinde controller’ın çağırdığı servis mock’lanır — zaten test edilen şey controller’dır.

@DataJpaTest bir ek davranış daha getirir: her testi transaction içinde çalıştırır ve sonunda geri alır. Testler birbirinin verisini görmez.

Kafam karıştı, daha basit anlat

Dilim, uygulamanın yalnızca bir parçasını açmaktır: @WebMvcTest sadece web katmanını, @DataJpaTest sadece veritabanı katmanını. Daha az şey açılır, test daha hızlı başlar.

Hızlı kontrolBaşlangıç

`@SpringBootTest` ile `@WebMvcTest` arasındaki fark nedir?

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

Asıl kazanç: geri bildirim süresi. Simülatördeki ikinci adım dersin özeti. Toplam süre değişmedi ama tek sınıfı çalıştırmak 14.4 sn’den 2.9 sn’ye düştü.

Bir geliştirici gün içinde tüm suite’i birkaç kez, tek bir test sınıfını ise onlarca kez çalıştırır. Optimize edilmesi gereken sayı budur.

İkinci kazanç ölçülemez ama daha değerlidir: izolasyon. @WebMvcTest kırmızıysa web katmanı bozuktur. @SpringBootTest kırmızıysa her şey olabilir — ve hata ayıklama oradan başlar.

Hızlı kontrolOrta

`@WebMvcTest(UserController.class)` çalışırken hangi bean'ler context'te bulunur?

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

Sınıflandırılmamış

Context'te var

Dilim bunları yükler

    Yok — mock'lanmalı

    @MockitoBean ile verilir

      Tuzaklar: çok fazla farklı ayar

      Cache anahtarı yapılandırmadır. Aşağıdakilerin her biri yeni bir context demektir:

      @MockitoBean PaymentClient paymentClient; // farklı bean grafiği
      @ActiveProfiles("integration") // farklı profil
      @TestPropertySource(properties = "...") // farklı property
      @SpringBootTest(webEnvironment = RANDOM_PORT) // farklı web ortamı

      Otuz test sınıfının her biri kendi @MockitoBean kombinasyonunu kullanıyorsa, otuz kez uygulama başlatırsın. Suite’in on dakika sürmesinin sebebi @SpringBootTest değil, otuz farklı yapılandırmadır.

      Pratik kural: mock kombinasyonlarını standartlaştır. Ortak bir @TestConfiguration veya bir temel sınıf, tüm entegrasyon testlerinin aynı context’i paylaşmasını sağlar.

      İlginç bir sonuç: simülatörde 3. kurulumda @MockitoBean eklemek hiçbir şeye mal olmuyor — çünkü o context’i zaten tek bir sınıf kullanıyordu. Bean override’ın maliyeti override’ın kendisinde değil, paylaşılan bir context’i bölmesindedir.

      Servis testlerine Spring gerekmez. En büyük kazanç aslında en basit olanı:

      class PricingServiceTest {
      private final DiscountPolicy policy = mock(DiscountPolicy.class);
      private final PricingService service = new PricingService(policy);
      @Test
      void appliesDiscount() { ... } // context yok, 10 ms
      }

      Constructor injection kullanıyorsan servis sınıfı düz bir Java nesnesidir. Onu test etmek için uygulama başlatmak, çekiç yerine vinç kullanmaktır.

      Bu, field injection’dan kaçınmanın en somut gerekçesidir: @Autowired bir alana yazıldığında sınıfı Spring olmadan kuramazsın.

      Kafam karıştı, daha basit anlat

      Her farklı ayar, yeni bir test ortamı demektir. Ayarları birkaç ortak sınıfta topla ki Spring aynı ortamı tekrar tekrar kullanabilsin.

      Hızlı kontrolİleri

      `@MockBean` ile `Mockito.mock()` arasındaki fark nedir?

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

      Testcontainers

      H2 hızlıdır ama Postgres değildir. Farklı SQL lehçesi, farklı kilitleme davranışı, farklı tip sistemi — üretimde patlayan sorgu testte geçer.

      Testcontainers gerçek veritabanını kullanır. Bedeli konteyner başlatma süresidir ve simülatörde bu açıkça görünür.

      Çözüm, konteyneri context başına değil JVM başına bir kez başlatmaktır:

      abstract class IntegrationTestBase {
      static final PostgreSQLContainer<?> DB = new PostgreSQLContainer<>("postgres:16");
      static { DB.start(); } // bir kez başlar, JVM kapanınca Ryuk temizler
      @DynamicPropertySource
      static void props(DynamicPropertyRegistry registry) {
      registry.add("spring.datasource.url", DB::getJdbcUrl);
      }
      }

      Bu desen aynı zamanda tüm entegrasyon testlerini tek bir temel sınıfta toplayarak context sayısını da düşürür — iki sorunu birden çözer.

      Bir banka hesap servisinde bu katmanlar nasıl yan yana durur, bakalım. Her test yalnızca ihtiyaç duyduğu kadarını açıyor.

      Derinleş · Hesap servisi: her katmana kendi testi 4 dosya · ~72 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/test/java/com/bank/account/ AccountControllerTest.java Yalnızca web katmanı açılır; servis bir mock.

      src/test/java/com/bank/account/AccountControllerTest.java
      @WebMvcTest(AccountController.class)
      class AccountControllerTest {
      @Autowired
      MockMvc mvc;
      @MockitoBean
      AccountService accounts;
      @Test
      @WithMockUser("C-1001")
      void returnsBalanceWithCurrency() throws Exception {
      when(accounts.balance("C-1001", "TR330006100519786457841326"))
      .thenReturn(new Money(new BigDecimal("1250.40"), Currency.getInstance("TRY")));
      // Amounts travel as strings, so no client ever parses them into a double.
      mvc.perform(get("/accounts/TR330006100519786457841326/balance"))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.amount").value("1250.40"))
      .andExpect(jsonPath("$.currency").value("TRY"));
      }
      }

      src/test/java/com/bank/ PostgresTestBase.java Postgres konteyneri JVM başına bir kez başlar; bu sınıfı genişleten testler aynı context'i paylaşır.

      src/test/java/com/bank/PostgresTestBase.java
      public abstract class PostgresTestBase {
      // One container per JVM; Ryuk removes it when the JVM exits.
      static final PostgreSQLContainer<?> DB = new PostgreSQLContainer<>("postgres:16");
      static {
      DB.start();
      }
      @DynamicPropertySource
      static void datasource(DynamicPropertyRegistry registry) {
      registry.add("spring.datasource.url", DB::getJdbcUrl);
      registry.add("spring.datasource.username", DB::getUsername);
      registry.add("spring.datasource.password", DB::getPassword);
      }
      }

      src/test/java/com/bank/ledger/ LedgerEntryRepositoryTest.java Gerçek Postgres üzerinde bakiye sorgusu; her test sonunda geri alınır.

      src/test/java/com/bank/ledger/LedgerEntryRepositoryTest.java
      @DataJpaTest
      @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // keep the real Postgres
      class LedgerEntryRepositoryTest extends PostgresTestBase {
      private static final String IBAN = "TR330006100519786457841326";
      @Autowired
      LedgerEntryRepository ledger;
      @Test
      void balanceIsCreditsMinusDebits() {
      ledger.save(LedgerEntry.credit(IBAN, new BigDecimal("1000.00"), "TRY"));
      ledger.save(LedgerEntry.debit(IBAN, new BigDecimal("249.60"), "TRY"));
      assertThat(ledger.balanceOf(IBAN, "TRY")).isEqualByComparingTo("750.40");
      }
      }

      src/test/java/com/bank/transfer/ TransferServiceTest.java Servis testi Spring'siz: new ile kurulur, uygulama hiç başlamaz.

      src/test/java/com/bank/transfer/TransferServiceTest.java
      class TransferServiceTest {
      private static final String FROM = "TR330006100519786457841326";
      private static final String TO = "TR360001000123456789012345";
      private final LedgerEntryRepository ledger = mock(LedgerEntryRepository.class);
      private final TransferService transfers = new TransferService(ledger);
      @Test
      void refusesATransferAboveTheBalance() {
      when(ledger.balanceOf(FROM, "TRY")).thenReturn(new BigDecimal("100.00"));
      assertThatThrownBy(() -> transfers.send(FROM, TO, new BigDecimal("150.00"), "TRY"))
      .isInstanceOf(InsufficientFundsException.class);
      verify(ledger, never()).save(any());
      }
      }

      Kendini sına

      Şimşek turu1/5

      Aynı ayarları kullanan test sınıfları, Spring'in tek bir açılışını paylaşır.

      Soru 1/3Orta

      Bir projede 8 test sınıfı var ve hepsi aynı yapılandırmayla `@SpringBootTest` kullanıyor. Testler çalışırken Spring context kaç kez başlar?

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

      Aklında kalacak üç şey

      1. 1 '@SpringBootTest yavaştır' tam doğru değil: aynı ayarlardaki test sınıfları tek bir açılışı paylaşır. Asıl soru kaç farklı açılış olduğudur.
      2. 2 Dilimin kazandırdığı şey toplam süre değil, tek bir sınıfı çalıştırma süresi ve netlik. @WebMvcTest patlarsa sorun web katmanındadır.
      3. 3 Yavaş testlerin bir numaralı sebebi fazla sayıda farklı ayar: @MockitoBean, @ActiveProfiles ve @TestPropertySource her biri yeni bir açılış demektir.
      Sonraki kapı Dört test sınıfı, tek PostgreSQL. İkinci sınıf neden 'Connection refused' diyor? Testcontainers — Gerçek Veritabanı, Tek Konteyner, Kilitli Kapı · 9 dk

      4 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı