Eğitim Portalı/Spring Boot/Test
Spring Boot03-spring-boot/07-test

Test

Çalışan bir kod yazmak yetmez; çalışmaya devam ettiğini garanti etmek gerekir. Testler, kodun doğruluğunu otomatik olarak doğrular; bir değişiklik bir şeyi bozduğunda anında haber verir. Bu güven, projeyi korkusuzca g…

Test

Çalışan bir kod yazmak yetmez; çalışmaya devam ettiğini garanti etmek gerekir. Testler, kodun doğruluğunu otomatik olarak doğrular; bir değişiklik bir şeyi bozduğunda anında haber verir. Bu güven, projeyi korkusuzca geliştirmenin temelidir. Spring Boot, hızlı birim testlerinden tam entegrasyon testlerine kadar zengin bir test desteği sunar.

Testler normalde gradle test veya IDE tarafından çalıştırılır (test sınıflarının main'i yoktur). Bu portalda görebilmen için örnekler, içlerindeki @Test sınıflarını JUnit Platform Launcher ile programatik çalıştırıp sonucu (geçen/kalan) yazar.

Test piramidi

İyi bir test stratejisi katmanlıdır:

  • Birim testler (çok ve hızlı): Tek bir sınıf/metodu yalıtılmış test eder (bağımlılıklar sahte). Saniyenin altında çalışır.
  • Dilim (slice) testleri: Uygulamanın bir katmanını yükler (@WebMvcTest web, @DataJpaTest veri).
  • Entegrasyon testleri (az ve yavaş): Tüm uygulamayı ayağa kaldırır (@SpringBootTest).

Birim test: JUnit 5 + AssertJ

Temel araç JUnit 5'tir: @Test ile test metodu, @BeforeEach/@AfterEach ile kurulum/temizlik. İddialar için AssertJ akıcı ve okunaklı bir API sunar:

@Test void indirim() {
    sepet.ekle(200);
    assertThat(sepet.indirimliToplam(0.10)).isEqualTo(180);
}
@Test void negatif() {
    assertThatThrownBy(() -> sepet.ekle(-5)).isInstanceOf(IllegalArgumentException.class);
}

Örnek 1 (./Ornek1.java) bir Sepet sınıfının üç davranışını test eder ve "3/3 GEÇTİ" özetini yazar.

Yalıtılmış test: Mockito

Bir servisi test ederken bağımlılıklarını (repository, dış API) gerçek nesnelerle çağırmak istemezsin (yavaş, kırılgan). Mockito ile sahte (mock) bağımlılık oluşturur, davranışını programlar ve etkileşimi doğrularsın:

UrunRepository sahte = mock(UrunRepository.class);
when(sahte.adBul(1)).thenReturn(Optional.of("Klavye")); // davranış
assertThat(servis.adGetir(1)).isEqualTo("Klavye");
verify(sahte).adBul(1);                                  // etkileşim doğrulama

Örnek 2 (./Ornek2.java) bir servisi sahte repository ile yalıtılmış test eder. Spring'de bir bean'i sahteyle değiştirmek için @MockBean kullanılır.

Web katmanı testi: MockMvc

Bir controller'ı test etmek için gerçek bir sunucu başlatmaya gerek yok. MockMvc, HTTP isteklerini taklit eder ve yanıtı doğrular:

MockMvc mvc = MockMvcBuilders.standaloneSetup(new UrunController()).build();
mvc.perform(get("/urun/1"))
   .andExpect(status().isOk())
   .andExpect(content().string(containsString("Klavye")));

Örnek 3 (./Ornek3.java) bir controller'ı sunucusuz, MockMvc ile test eder.

Spring Boot test anotasyonları

Gerçek projelerde testleri Spring'in test desteğiyle yazarsın:

  • @SpringBootTest — Tüm uygulama context'ini yükler; uçtan uca entegrasyon testi. İsteğe bağlı webEnvironment = RANDOM_PORT ile gerçek sunucu + TestRestTemplate/WebTestClient.
  • @WebMvcTest(UrunController.class) — Yalnızca web katmanını yükler (controller + MockMvc), servis/repository @MockBean ile sahtelenir. Hızlı web testi.
  • @DataJpaTest — Yalnızca JPA katmanını + gömülü bir veritabanını yükler; repository'leri gerçek sorgularla test eder, sonunda rollback yapar.
  • @MockBean / @SpyBean — Context'teki bir bean'i sahte/casus ile değiştirir.
@WebMvcTest(UrunController.class)
class UrunControllerTest {
    @Autowired MockMvc mvc;
    @MockBean UrunService service;   // servis sahtelenir
    @Test void test() throws Exception { mvc.perform(get("/urun/1")).andExpect(status().isOk()); }
}

Testcontainers

Entegrasyon testlerinde gömülü H2 yerine gerçek bir veritabanına (PostgreSQL, MySQL) karşı test etmek istiyorsan, Testcontainers testin başında Docker ile gerçek bir veritabanı konteyneri başlatır, sonunda kapatır. "Üretimdeki gibi" test ortamı sağlar.

Özet

Test piramidini; JUnit 5 + AssertJ ile birim testi (Örnek 1), Mockito ile yalıtılmış testi (Örnek 2) ve MockMvc ile web katmanı testini (Örnek 3) çalışan biçimde gördük; @SpringBootTest, @WebMvcTest, @DataJpaTest, @MockBean ve Testcontainers kavramlarını ele aldık. Testler, yazdığın her şeyin güvencesidir.

Sırada, mikroservis dünyasının en zorlu konularından biri var: birden çok servise yayılan işlemlerde tutarlılık — dağıtık transaction ve Saga pattern.

Kod Örnekleri(3)

Ornek1

ortam gerekir
Ornek1.java
1// Ornek1: Birim test (unit test) — JUnit 5 + AssertJ ile saf iş mantığını test etmek.
2// Bu dosya, içindeki @Test sınıfını JUnit Platform Launcher ile çalıştırıp sonucu yazar.
3// (Normalde testleri 'gradle test' / IDE çalıştırır; burada portalda görebilmen için programatik.)
4// Çalıştırma: portal derleyip çalıştırır.
5package com.egitim.springboot.test;
6
7import org.junit.jupiter.api.BeforeEach;
8import org.junit.jupiter.api.DisplayName;
9import org.junit.jupiter.api.Test;
10import org.junit.platform.launcher.Launcher;
11import org.junit.platform.launcher.LauncherDiscoveryRequest;
12import org.junit.platform.launcher.core.LauncherFactory;
13import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
14import org.junit.platform.launcher.listeners.TestExecutionSummary;
15
16import java.io.PrintWriter;
17
18import static org.assertj.core.api.Assertions.assertThat;
19import static org.assertj.core.api.Assertions.assertThatThrownBy;
20import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
21import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
22
23public class Ornek1 {
24
25    // --- Test edilen sınıf (saf iş mantığı, Spring'e ihtiyaç yok) ---
26    static class Sepet {
27        private double toplam = 0;
28        void ekle(double fiyat) {
29            if (fiyat < 0) throw new IllegalArgumentException("fiyat negatif olamaz");
30            toplam += fiyat;
31        }
32        double indirimliToplam(double oran) { return toplam * (1 - oran); }
33        double getToplam() { return toplam; }
34    }
35
36    // --- Test sınıfı: @Test metotları + AssertJ iddiaları (assertions) ---
37    static class SepetTest {
38        Sepet sepet;
39
40        @BeforeEach           // her testten önce taze kurulum
41        void setUp() { sepet = new Sepet(); }
42
43        @Test
44        @DisplayName("ürün ekleyince toplam artar")
45        void urunEkleme() {
46            sepet.ekle(100);
47            sepet.ekle(50);
48            assertThat(sepet.getToplam()).isEqualTo(150);
49        }
50
51        @Test
52        @DisplayName("indirim doğru hesaplanır")
53        void indirim() {
54            sepet.ekle(200);
55            assertThat(sepet.indirimliToplam(0.10)).isEqualTo(180);
56        }
57
58        @Test
59        @DisplayName("negatif fiyat reddedilir")
60        void negatifFiyat() {
61            assertThatThrownBy(() -> sepet.ekle(-5))
62                    .isInstanceOf(IllegalArgumentException.class)
63                    .hasMessageContaining("negatif");
64        }
65    }
66
67    public static void main(String[] args) {
68        System.out.println("================ BİRİM TEST (JUnit 5 + AssertJ) ================");
69        testCalistir(SepetTest.class);
70        System.out.println("""
71
72                --- Birim test ---
73                @Test ile test metodu, @BeforeEach ile her testten önce kurulum yazılır.
74                AssertJ akıcı iddialar sunar: assertThat(x).isEqualTo(y), assertThatThrownBy(...).
75                Birim testler hızlıdır ve TEK bir sınıfı/metodu yalıtılmış test eder.""");
76    }
77
78    // JUnit testlerini programatik çalıştırıp özet yazan yardımcı.
79    static void testCalistir(Class<?> testSinifi) {
80        LauncherDiscoveryRequest req = request().selectors(selectClass(testSinifi)).build();
81        Launcher launcher = LauncherFactory.create();
82        SummaryGeneratingListener dinleyici = new SummaryGeneratingListener();
83        launcher.registerTestExecutionListeners(dinleyici);
84        launcher.execute(req);
85        TestExecutionSummary ozet = dinleyici.getSummary();
86        System.out.printf("Çalışan: %d | Başarılı: %d | Başarısız: %d%n",
87                ozet.getTestsStartedCount(), ozet.getTestsSucceededCount(), ozet.getTestsFailedCount());
88        ozet.getFailures().forEach(f -> System.out.println("  HATA: " + f.getException()));
89        if (ozet.getTestsFailedCount() == 0) System.out.println("Tüm testler GEÇTİ ✓");
90        ozet.printFailuresTo(new PrintWriter(System.out));
91    }
92}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: Mockito ile yalıtılmış test — bağımlılığı sahte (mock) nesneyle değiştirmek.
2// Çalıştırma: portal derleyip çalıştırır (JUnit Platform Launcher ile).
3package com.egitim.springboot.test;
4
5import org.junit.jupiter.api.Test;
6import org.junit.platform.launcher.Launcher;
7import org.junit.platform.launcher.LauncherDiscoveryRequest;
8import org.junit.platform.launcher.core.LauncherFactory;
9import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
10import org.junit.platform.launcher.listeners.TestExecutionSummary;
11
12import java.io.PrintWriter;
13import java.util.Optional;
14
15import static org.assertj.core.api.Assertions.assertThat;
16import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
17import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
18import static org.mockito.Mockito.*;
19
20public class Ornek2 {
21
22    // Test edilecek servis, bir repository'ye bağımlı.
23    interface UrunRepository { Optional<String> adBul(int id); }
24
25    static class UrunServisi {
26        private final UrunRepository repo;
27        UrunServisi(UrunRepository repo) { this.repo = repo; }
28        String adGetir(int id) { return repo.adBul(id).orElse("bilinmiyor"); }
29    }
30
31    static class UrunServisiTest {
32        @Test
33        void bulunanUrun() {
34            // 1) Sahte repository oluştur (gerçek DB'ye gitmez).
35            UrunRepository sahteRepo = mock(UrunRepository.class);
36            // 2) Davranışını programla: id=1 için "Klavye" dön.
37            when(sahteRepo.adBul(1)).thenReturn(Optional.of("Klavye"));
38
39            UrunServisi servis = new UrunServisi(sahteRepo);
40            assertThat(servis.adGetir(1)).isEqualTo("Klavye");
41
42            // 3) Etkileşimi doğrula: repo.adBul(1) gerçekten çağrıldı mı?
43            verify(sahteRepo).adBul(1);
44        }
45
46        @Test
47        void bulunmayanUrun() {
48            UrunRepository sahteRepo = mock(UrunRepository.class);
49            when(sahteRepo.adBul(anyInt())).thenReturn(Optional.empty()); // hiçbir id bulunamasın
50            UrunServisi servis = new UrunServisi(sahteRepo);
51            assertThat(servis.adGetir(99)).isEqualTo("bilinmiyor");
52        }
53    }
54
55    public static void main(String[] args) {
56        System.out.println("================ MOCKITO (yalıtılmış test) ================");
57        testCalistir(UrunServisiTest.class);
58        System.out.println("""
59
60                --- Mockito ---
61                mock(...) ile sahte bağımlılık; when(...).thenReturn(...) ile davranış programlama;
62                verify(...) ile etkileşim doğrulama. Böylece servisi DB/ağ olmadan, yalıtılmış test edersin.
63                Spring'de @MockBean ile bir bean'i sahteyle değiştirip dilim (slice) testlerinde kullanırsın.""");
64    }
65
66    static void testCalistir(Class<?> testSinifi) {
67        LauncherDiscoveryRequest req = request().selectors(selectClass(testSinifi)).build();
68        Launcher launcher = LauncherFactory.create();
69        SummaryGeneratingListener dinleyici = new SummaryGeneratingListener();
70        launcher.registerTestExecutionListeners(dinleyici);
71        launcher.execute(req);
72        TestExecutionSummary ozet = dinleyici.getSummary();
73        System.out.printf("Çalışan: %d | Başarılı: %d | Başarısız: %d%n",
74                ozet.getTestsStartedCount(), ozet.getTestsSucceededCount(), ozet.getTestsFailedCount());
75        if (ozet.getTestsFailedCount() == 0) System.out.println("Tüm testler GEÇTİ ✓");
76        ozet.printFailuresTo(new PrintWriter(System.out));
77    }
78}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek3

ortam gerekir
Ornek3.java
1// Ornek3: MockMvc ile web katmanı testi — controller'ı sunucu başlatmadan test etmek.
2// Çalıştırma: portal derleyip çalıştırır (JUnit Platform Launcher ile).
3package com.egitim.springboot.test;
4
5import org.junit.jupiter.api.Test;
6import org.junit.platform.launcher.Launcher;
7import org.junit.platform.launcher.LauncherDiscoveryRequest;
8import org.junit.platform.launcher.core.LauncherFactory;
9import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
10import org.junit.platform.launcher.listeners.TestExecutionSummary;
11import org.springframework.test.web.servlet.MockMvc;
12import org.springframework.test.web.servlet.setup.MockMvcBuilders;
13import org.springframework.web.bind.annotation.GetMapping;
14import org.springframework.web.bind.annotation.PathVariable;
15import org.springframework.web.bind.annotation.RestController;
16
17import java.io.PrintWriter;
18
19import static org.hamcrest.Matchers.containsString;
20import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
21import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
22import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
24import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
25
26public class Ornek3 {
27
28    // Test edilecek controller.
29    @RestController
30    static class UrunController {
31        @GetMapping("/urun/{id}")
32        String urun(@PathVariable int id) {
33            return id == 1 ? "Klavye" : "urun yok";
34        }
35    }
36
37    static class UrunControllerTest {
38        // standaloneSetup: tam sunucu/context başlatmadan, sadece controller'ı MockMvc'ye bağlar.
39        private final MockMvc mvc = MockMvcBuilders.standaloneSetup(new UrunController()).build();
40
41        @Test
42        void mevcutUrun() throws Exception {
43            // HTTP isteğini taklit et ve yanıtı doğrula (gerçek ağ/port yok).
44            mvc.perform(get("/urun/1"))
45                    .andExpect(status().isOk())
46                    .andExpect(content().string(containsString("Klavye")));
47        }
48
49        @Test
50        void olmayanUrun() throws Exception {
51            mvc.perform(get("/urun/99"))
52                    .andExpect(status().isOk())
53                    .andExpect(content().string(containsString("yok")));
54        }
55    }
56
57    public static void main(String[] args) {
58        System.out.println("================ MOCKMVC (web katmanı testi) ================");
59        testCalistir(UrunControllerTest.class);
60        System.out.println("""
61
62                --- MockMvc ---
63                Controller'ı GERÇEK sunucu başlatmadan, HTTP isteklerini taklit ederek test eder.
64                perform(get(...)).andExpect(status().isOk()) ... ile yanıtı doğrularsın. Hızlıdır.
65                Spring'de @WebMvcTest ile yalnızca web katmanını yükleyen dilim testleri yazılır.""");
66    }
67
68    static void testCalistir(Class<?> testSinifi) {
69        LauncherDiscoveryRequest req = request().selectors(selectClass(testSinifi)).build();
70        Launcher launcher = LauncherFactory.create();
71        SummaryGeneratingListener dinleyici = new SummaryGeneratingListener();
72        launcher.registerTestExecutionListeners(dinleyici);
73        launcher.execute(req);
74        TestExecutionSummary ozet = dinleyici.getSummary();
75        System.out.printf("Çalışan: %d | Başarılı: %d | Başarısız: %d%n",
76                ozet.getTestsStartedCount(), ozet.getTestsSucceededCount(), ozet.getTestsFailedCount());
77        if (ozet.getTestsFailedCount() == 0) System.out.println("Tüm testler GEÇTİ ✓");
78        ozet.printFailuresTo(new PrintWriter(System.out));
79    }
80}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.