Gökmen Tuksavul

Blog Yazısı

2026-07-26

JUnit 5 ve Mockito ile Test Stratejileri

Spring Boot uygulamalarında JUnit 5 ve Mockito kullanarak profesyonel birim test (unit test) ve entegrasyon testi yazma stratejilerini kod örnekleriyle öğrenin.

TestingJUnitMockito

Sponsorlu

Spring Boot projelerinde test stratejisi yüksek coverage sayısına indirgenmemelidir. Amaç, değişiklik yapıldığında hangi davranışın bozulduğunu hızlı ve güvenilir şekilde görebilmektir. Bu nedenle unit test, slice test ve entegrasyon testlerinin görevleri ayrılmalıdır.

Unit test

Unit test framework bağımlılığını minimumda tutmalıdır. Domain servisleri, validasyon kuralları ve hesaplama fonksiyonları Spring context açmadan test edilebiliyorsa bu tercih edilmelidir. Böylece testler hızlı çalışır ve hata yeri daha net görünür.

Mockito sınırı

Mockito dış bağımlılıkları kontrol etmek için kullanılmalıdır: repository, HTTP client veya message publisher gibi. Value object, mapper veya saf domain fonksiyonlarını mocklamak testleri gereksiz kırılgan hale getirir.

Slice test

WebMvcTest ve DataJpaTest gibi slice testler belirli katmanın framework entegrasyonunu doğrular. Controller için request validation, status code ve response formatı bu seviyede test edilebilir. Tüm uygulama contextini açmaya gerek kalmaz.

Entegrasyon testi

SpringBootTest daha yavaştır ama migration, transaction sınırı, security filter zinciri veya gerçek repository davranışı için değerlidir. Testcontainers kullanmak production veritabanı davranışına daha yakın sonuç verir.

Sonuç

İyi test stratejisi doğru seviyede test yazmaktır. Hızlı unit testler, dar kapsamlı slice testler ve sınırlı ama anlamlı entegrasyon testleri birlikte kullanıldığında hem güven hem teslimat hızı korunur.

Pratik uygulama notu

Test yazarken en iyi başlangıç noktası geçmişte hata üretmiş davranışlardır. Önce fiyat hesaplama, yetki kontrolü, tarih kuralı ve dış servis hatası gibi iş açısından riskli kararlar korunmalıdır. Daha sonra controller validation ve repository sorguları eklenebilir. Böyle ilerlemek coverage sayısından daha anlamlı bir güvenlik ağı oluşturur.

Test Piramidini Risk Üzerinden Kurun

Her sınıf için aynı sayıda test yazmak yerine hatanın etkisine ve değişim sıklığına bakın. Fiyat hesaplama, yetkilendirme veya para transferi gibi saf iş kuralları çok sayıda hızlı unit test hak eder. Veritabanı mapping'i, transaction sınırı ve güvenlik filtresi gerçek framework davranışına bağlı olduğu için entegrasyon testi gerektirir. Birkaç kritik kullanıcı akışı uçtan uca test edilir; bütün varyasyonları tarayıcı testine taşımak suite'i yavaş ve kırılgan yapar.

İyi bir unit test uygulama ayrıntısını değil gözlenebilir davranışı doğrular. Servisin özel metodunu çağırmak veya her satır için mock interaction yazmak refactor sırasında gereksiz kırılma üretir. Girdi, sonuç ve önemli yan etki üzerinden test kurun. Test adı senaryoyu ve beklenen sonucu anlatmalıdır: expiredCoupon_doesNotChangeOrderTotal gibi.

Mockito'yu Sınırda Kullanın

Değer nesneleri ve domain modelleri mock'lanmamalıdır; gerçek nesne daha anlaşılırdır. Ağ, saat, rastgele sayı üretici veya harici ödeme gateway'i gibi kontrolünüz dışındaki sınırlar mock/fake için uygundur. Çok sayıda deep stub kullanmak test edilen sınıfın fazla sorumluluk taşıdığını gösterebilir.

@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
    @Mock PaymentGateway gateway;
    @Mock Clock clock;
    @InjectMocks PaymentService service;

@Test void declinedPayment_keepsOrderUnpaid() { var order = Order.awaitingPayment("order-42", Money.tryOf("120.00")); when(gateway.charge(any())).thenReturn(ChargeResult.declined("insufficient_funds"));

var result = service.pay(order);

assertThat(result.status()).isEqualTo(PaymentStatus.DECLINED); assertThat(order.isPaid()).isFalse(); verify(gateway).charge(argThat(request -> request.orderId().equals("order-42"))); } } ```

verifyNoMoreInteractions gibi katı doğrulamaları her testte kullanmak, zararsız logging veya telemetry eklenince testleri kırabilir. Yalnızca iş açısından önemli çağrıları doğrulayın. Buna karşılık ödeme iki kez çekilmemeli gibi negatif bir gereksinim varsa çağrı sayısı doğrudan davranışın parçasıdır.

Entegrasyon Testinde Gerçek Bağımlılık

H2 üzerinde geçen test PostgreSQL'e özgü JSONB, collation, sequence veya transaction davranışını yakalamayabilir. Testcontainers ile üretimde kullanılan veritabanının uyumlu sürümünü çalıştırın. Migration'ları test başlangıcında uygulayarak şemanın sıfırdan kurulabildiğini de doğrulayın. Her test için container başlatmak yerine suite seviyesinde yeniden kullanım ile hız kazanabilirsiniz; test verisi izolasyonunu transaction veya kontrollü cleanup ile sağlayın.

@SpringBootTest bütün context'i yüklediği için her testte varsayılan olmamalıdır. Repository için @DataJpaTest, MVC sözleşmesi için @WebMvcTest gibi slice testler daha hızlı geri bildirim verir. Ancak güvenlik zinciri, serialization ve gerçek bean wiring birlikte doğrulanacaksa tam context testi bilinçli tercihtir.

Kararlı Testler İçin Saat ve Eşzamanlılık

Instant.now() çağrısını domain içine yaymak yerine Clock enjekte edin. Böylece son kullanma tarihi ve zaman dilimi senaryoları uyku eklemeden test edilir. Asenkron işlemlerde sabit Thread.sleep yerine belirli koşulu süre sınırı içinde bekleyen araçlar kullanın. Testin bazen geçmesi, bazen kalması genellikle gerçek bir yarış koşulunu gizler.

CI raporunda yalnızca coverage yüzdesine bakmayın. Mutation testing veya kritik branch listesi testlerin gerçekten hata yakalayıp yakalamadığını daha iyi gösterebilir. Yüzde 90 coverage, assertion içermeyen testlerle de elde edilebilir. Amaç satır çalıştırmak değil, riskli davranışta istenmeyen değişikliği hızlı ve güvenilir biçimde durdurmaktır.