İçeriğe geç

Test Piramidi, JUnit 5 ve Mockito

Başlangıç 10 dk Sık karşılaşılır

30 saniyede özet

Test piramidi güzel bir çizim değil, bir hız ve maliyet hesabı: altta çok sayıda hızlı test, üstte az sayıda yavaş test. Doğru soru şu: bu hatayı görebilecek en ucuz test hangisi?

Test piramidi genellikle güzel bir üçgen olarak çizilir. Ama asıl sebebi çok daha somut: maliyet ve cevabı ne kadar bekleyeceğin.

  1. Bayt: Bütün testlerimizi tarayıcıda, gerçek kullanıcı gibi yazdık. Her şey kapsanıyor!

  2. Sen: Süper. Bir satır değiştirince ne kadar bekliyorsun?

  3. Bayt: Yirmi dakika... Bazen de sebepsiz kırmızı yanıyor.

  4. Bayt: Gevşek bir vida için her seferinde pistte tur atmak mantıklı mı?

Tezgâh, motor, pist: her kontrolün yeri ve fiyatı farklı.
Adım adım oku
  1. Birim testi parçayı tezgâhta tek başına sınar; çok hızlıdır ve çok sayıda yazılır.
  2. Entegrasyon testi parçaları birlikte çalıştırır; birim testinin göremediği bağlantı hatalarını yakalar.
  3. Uçtan uca test arabayı pistte sürer; en gerçekçi olanıdır ama yavaştır ve bazen sebepsiz kırılır.
  4. Piramit bunun sonucudur: altta çok sayıda ucuz test, üstte az sayıda pahalı test.
KatmanTipik süreNeyi görürKırılganlık
Birim~5 msSaf mantıkYok
Entegrasyon~250 msBağlantı, sözleşme, SQLDüşük
Uçtan uca~4 snTüm kullanıcı akışıYüksek

Bir uçtan uca test, bir birim testinin yaklaşık 800 katı sürer. Piramidin sebebi budur.

Kendin ölç

Simülatör bir değişikliğe karşı suite’i alttan üste çalıştırıyor ve ilk gerçek hatada duruyor. İzlenecek sayı: geri bildirim süresi.

Test piramidi — hatayı ne kadar sürede duyarsın

Tohum 7
Uçtan uca test0/54000 ms/test · kapsama %50
Entegrasyon testi0/30250 ms/test · kapsama %100
Birim testi0/2005 ms/test · kapsama %100
Geçen süre
0 ms
Geri bildirim
—
Tam suite
28.5 sn
Hız
Adım 0

Şu an ne oldu?

Birim testi — 0/200 · 0.0 sn

Bu katmanın bu hata için kapsaması %100. Test başına maliyet 5 ms.

Aklında kalsın: Piramit bir estetik tercih değil, geri bildirim süresi optimizasyonudur.

Olay günlüğü (0)

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

Karşılaştır:

  • Piramit + Hesaplama hatası — birim katmanı saniyenin altında yakalıyor.
  • Dondurma külahı + aynı hata. 20 birim testi bu hatayı kaçırıyor; aynı hata dakikalar sonra, çok daha pahalı bir katmanda ortaya çıkıyor. Kaçırma sebebi birim testlerinin zayıf olması değil, yeterince olmaması.
  • Piramit + Bean/transaction bağlantı hatası — birim katmanı bunu hiç göremiyor. Bu bir eksiklik değil, mock’un tanımı.
  • Dondurma külahı + Kullanıcı akışı — flakyKod değişmediği hâlde bazen geçen bazen kalan test. Verdiği bilgiyi yok eder: artık "geçti" ne anlama geliyor bilmezsin.Sebebi neredeyse her zaman üç şeyden biridir: paylaşılan durum, zamanlama (sabit bekleme), ya da test sırası bağımlılığı. Retry eklemek semptomu gizler ve bazen üründeki gerçek bir yarış durumunu örter.Sözlükte gör → başarısızlıklara dikkat et. Bunlar gerçek hata değil ama zaman ve dikkat harcatıyor.
Hızlı kontrolBaşlangıç

Testi "birim testi" yapan şey nedir?

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

Hangi katman neyi görür?

Servis birim testi repository'yi mock'luyor. Repository'deki JPQL sorgusunda bir alan adı yanlış yazılmış. Bu birim testi hatayı yakalar mı? Cevabı göster

Hayır. Mock gerçek sorguyu hiç çalıştırmaz. Bu hatayı yalnızca gerçek veritabanıyla koşan bir entegrasyon testi görür.

Bu tablo, “her şeyi birim test et” tavsiyesinin neden yanlış olduğunu gösterir:

Hata türüBirimEntegrasyonUçtan uca
Hesaplama hatası✓✓✓
Bean/transaction bağlantısı✕✓✓
API sözleşmesi bozuldu✕✓✓
Kullanıcı akışı kırıldı✕✕✓

Birim testi bağımlılıkları taklit eder — taklit ettiğin şeydeki bir hata birim testinde asla görünmez. Mock kullanmanın bedeli budur ve entegrasyon katmanının vazgeçilmez olmasının sebebi de.

Kafam karıştı, daha basit anlat

Birim testi taklit bir ortakla çalışır. Taklit gerçeğinden farklı davranıyorsa, birim testi bu farkı asla göremez. Bu yüzden birkaç gerçek entegrasyon testi de gerekir.

Hızlı kontrolOrta

Test piramidinin tersi olan "ice cream cone" hangi belirtileri üretir?

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

Taklitler: mock, stub, spy, fake

Dördü farklı işler yapar ve sık karıştırılır:

TürNe yaparNe zaman
StubSabit cevap dönerGirdi gerekiyor, davranış önemli değil
MockNasıl çağrıldığını doğrularEtkileşimin kendisi test konusu
SpyGerçek nesneyi sarar, bazı metotları izler/değiştirirLegacy kodda kısmi taklit
FakeBasit ama çalışan bir uygulamaIn-memory repository gibi
Stub — sadece veri sağlıyor
when(priceRepository.findBySku("A1")).thenReturn(new Price(100)); // cevabı kur
Mock — etkileşimi doğruluyor
orderService.place(order);
verify(emailSender).send(argThat(mail -> mail.to().equals("ali@example.com")));

Ayrım şu: fakeGerçeğinin basitleştirilmiş ama çalışan bir uygulaması — bellek içi bir repository gibi. Stub hazır cevap döner, mock ayrıca çağrıldığını doğrular.Sözlükte gör → girdi sağlar, mock çıktıyı doğrular. Mockito’da ikisi de mock() ile üretilir; farkı yaratan when mi yoksa verify mi yazdığındır.

Bu taklitler bir bankada da hep yan yana durur. Yurt dışı kart harcamasını TL’ye çevirip hesaptan düşen küçük bir serviste hangisinin nerede durduğuna bak.

Derinleş · Yurt dışı kart harcaması: stub, fake ve mock bir arada 4 dosya · ~109 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/card/ CardPaymentService.java Servis: kuru alır, tutarı TL'ye çevirir, hesaptan düşer ve müşteriye bildirim gönderir.

src/main/java/com/bank/card/CardPaymentService.java
final class CardPaymentService {
private static final Currency TRY = Currency.getInstance("TRY");
private final FxRates fxRates;
private final Ledger ledger;
private final SmsSender sms;
CardPaymentService(FxRates fxRates, Ledger ledger, SmsSender sms) {
this.fxRates = fxRates;
this.ledger = ledger;
this.sms = sms;
}
// A foreign card payment is booked in TRY on the customer's account.
Money pay(String accountNo, Money price) {
BigDecimal rate = fxRates.sellRate(price.currency()); // TRY per unit of foreign currency
Money charged = new Money(price.value().multiply(rate).setScale(2, RoundingMode.HALF_EVEN), TRY);
ledger.debit(accountNo, charged);
sms.paymentNotice(accountNo, charged);
return charged;
}
}

src/test/java/com/bank/card/ InMemoryLedger.java Fake: bakiyeleri bellekte tutan, gerçekten çalışan küçük bir defter.

src/test/java/com/bank/card/InMemoryLedger.java
// A fake: a real, working ledger that keeps balances in a map.
final class InMemoryLedger implements Ledger {
private final Map<String, BigDecimal> balances = new HashMap<>();
InMemoryLedger withBalance(String accountNo, String amount) {
balances.put(accountNo, new BigDecimal(amount));
return this;
}
@Override
public void debit(String accountNo, Money amount) {
BigDecimal balance = balances.getOrDefault(accountNo, BigDecimal.ZERO);
if (balance.compareTo(amount.value()) < 0) {
throw new InsufficientFundsException(accountNo);
}
balances.put(accountNo, balance.subtract(amount.value()));
}
BigDecimal balanceOf(String accountNo) {
return balances.get(accountNo);
}
}

src/test/java/com/bank/card/ CardPaymentServiceTest.java Kur bir stub, defter bir fake, SMS bir mock; sonuç ve bakiye doğrulanıyor.

src/test/java/com/bank/card/CardPaymentServiceTest.java
@ExtendWith(MockitoExtension.class)
class CardPaymentServiceTest {
private static final String ACCOUNT = "TR330006100519786457841326";
private static final Currency EUR = Currency.getInstance("EUR");
@Mock
FxRates fxRates; // a stub: it only supplies a rate
@Mock
SmsSender sms; // a mock: the notice is part of the contract
private final InMemoryLedger ledger = new InMemoryLedger().withBalance(ACCOUNT, "5000.00");
private CardPaymentService service;
@BeforeEach
void setUp() {
when(fxRates.sellRate(EUR)).thenReturn(new BigDecimal("37.4520"));
service = new CardPaymentService(fxRates, ledger, sms);
}
@Test
void chargesTheConvertedAmountAndNotifiesTheCustomer() {
Money charged = service.pay(ACCOUNT, new Money(new BigDecimal("40.00"), EUR));
assertThat(charged.value()).isEqualByComparingTo("1498.08"); // 40.00 x 37.4520
assertThat(ledger.balanceOf(ACCOUNT)).isEqualByComparingTo("3501.92");
verify(sms).paymentNotice(ACCOUNT, charged);
}
@Test
void sendsNoNoticeWhenTheBalanceIsShort() {
assertThatThrownBy(() -> service.pay(ACCOUNT, new Money(new BigDecimal("200.00"), EUR)))
.isInstanceOf(InsufficientFundsException.class);
verifyNoInteractions(sms);
}
}

src/test/java/com/bank/card/ CardPaymentServiceStrictTest.java Şöyle de yazılabilirdi: bugün geçer, ama ilk refactor'da sonuç aynı kalsa bile kırmızı yanar.

src/test/java/com/bank/card/CardPaymentServiceStrictTest.java
// It could also be written like this. It passes today; watch the first refactor.
@ExtendWith(MockitoExtension.class)
class CardPaymentServiceStrictTest {
private static final Currency EUR = Currency.getInstance("EUR");
private static final Currency TRY = Currency.getInstance("TRY");
@Mock FxRates fxRates;
@Mock Ledger ledger;
@Mock SmsSender sms;
@Test
void chargesTheConvertedAmount() {
when(fxRates.sellRate(EUR)).thenReturn(new BigDecimal("37.4520"));
new CardPaymentService(fxRates, ledger, sms).pay("TR330006100519786457841326",
new Money(new BigDecimal("40.00"), EUR));
// Every call, in this exact order, and nothing else. Read the balance first for a
// clearer error, and this goes red although the same amount is charged.
InOrder order = inOrder(fxRates, ledger, sms);
order.verify(fxRates).sellRate(EUR);
order.verify(ledger).debit(any(), eq(new Money(new BigDecimal("1498.08"), TRY)));
order.verify(sms).paymentNotice(any(), any());
verifyNoMoreInteractions(fxRates, ledger, sms);
}
}
Kafam karıştı, daha basit anlat

Stub hazır cevap verir, mock kendisine ne sorulduğunu kontrol eder. Spy gerçek nesneyi izler, fake ise basit ama çalışan bir kopyadır.

Hızlı kontrolBaşlangıç

Mock, stub ve fake arasındaki fark nedir?

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

JUnit 5’te bilmeye değer birkaç şey.

OrderServiceTest.java
@Test
@DisplayName("stok yetersizse sipariş reddedilir")
void rejectsWhenOutOfStock() {
var exception = assertThrows(OutOfStockException.class,
() -> orderService.place(order));
assertThat(exception.getMessage()).contains("SKU-1");
}
@ParameterizedTest
@CsvSource({ "0, false", "1, true", "99, true" })
void availability(int stock, boolean expected) {
assertThat(inventory.isAvailable(stock)).isEqualTo(expected);
}

JUnit 4’ten gelen iki değişiklik sık sorulur: @Before yerine @BeforeEach, ve @RunWith yerine @ExtendWith (Mockito için @ExtendWith(MockitoExtension.class)).

Hızlı kontrolBaşlangıç

Her ifadeyi doğru test çifti türüne yerleştir.

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

Sınıflandırılmamış

Stub

Girdi sağlar — sabit cevap döner

    Mock

    Çıktıyı doğrular — nasıl çağrıldığına bakar

      Tuzaklar: kırılgan test, testsizlikten kötüdür

      Simülatörde uçtan uca katmanın ürettiği kırılgan başarısızlıklara bak. Bir test bazen kırmızı bazen yeşilse:

      • Ekip onu ciddiye almayı bırakır
      • “Tekrar çalıştır” refleksi oluşur
      • Ve gerçek bir hata geldiğinde de tekrar çalıştırılır

      Kırılgan bir testi tamir et ya da sil. Kırmızı kalmasına izin vermek, bütün suite’e olan güveni aşındırır.

      Ne test edilmeli. Pratik bir filtre: test, kodun bir şey yapmasını değil, bir şeyi yanlış yapmamasını garanti etmeli.

      • Test et: iş kuralları, sınır değerler, hata yolları, düzelttiğin her hata
      • Test etme: getter/setter, framework’ün kendi davranışı, kendi mock’ladığın şey

      Bir hata bildirimi geldiğinde önce onu yeniden üreten kırmızı testi yazmak neredeyse her zaman doğrudur: hem düzelttiğini kanıtlar hem geri gelmesini engeller.

      Kendini sına

      Şimşek turu1/5

      Birim testi, mock'ladığın bağımlılıktaki bir hatayı yakalayabilir.

      Soru 1/3Başlangıç

      Test piramidinin asıl gerekçesi nedir?

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

      Aklında kalacak üç şey

      1. 1 Piramidin sebebi maliyet ve bekleme süresidir. Bir uçtan uca test, bir birim testinden yaklaşık 800 kat uzun sürer ve daha kolay bozulur.
      2. 2 Doğru soru 'her şeyi birim test et' değil, 'bu hatayı görebilecek en ucuz katman hangisi?'. Bağlantı hatasını birim testi zaten göremez.
      3. 3 Mock'un bedeli: taklit ettiğin şeyin gerçekten öyle davrandığını test etmemiş olursun. Entegrasyon testleri bu yüzden vazgeçilmezdir.
      Sonraki kapı Henüz yazılmamış bir kodun testini önce yazmak neden işe yarasın? TDD — Red, Green, Refactor · 10 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.