← Kursa Dön
📄 Text · 30 min

Mockito: Mock ve Stub

Birim testlerin temel prensibi izolasyondur: test ettiğiniz sınıf, dış bağımlılıklarından (veritabanı, HTTP istemci, dosya sistemi) ayrılmalıdır. Mockito, Java ekosisteminin en popüler mocking framework'üdür ve Spring Boot ile sorunsuz çalışır. Bu derste Mockito'nun tüm temel ve ileri özelliklerini öğreneceğiz.

Mock, Stub, Spy — Temel Kavramlar

Bu üç kavram sıkça karıştırılır. Net olarak ayıralım:

  • Mock: Gerçek nesnenin "sahte" kopyasıdır. Tüm metotları tanımlıdır ama varsayılan olarak hiçbir şey yapmaz (null/0/false döner). Çağrıları kaydeder — sonra doğrulayabilirsiniz.

  • Stub: Belirli bir girdi için önceden tanımlanmış bir çıktı döndüren yapıdır. when(...).thenReturn(...) ile oluşturulur.

  • Spy: Gerçek nesneyi saran bir wrapper'dır. Varsayılan olarak gerçek metotları çağırır, sadece belirli metotları override edebilirsiniz.

Mockito Kurulumu

Spring Boot projelerinde Mockito spring-boot-starter-test ile gelir:

<!-- Otomatik olarak dahildir -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

@Mock ve @InjectMocks

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class) // Mockito anotasyonlarını aktifleştir
class OrderServiceTest {

    @Mock
    private ProductRepository productRepository; // Sahte repository

    @Mock
    private NotificationService notificationService; // Sahte notification

    @InjectMocks
    private OrderService orderService; // Mock'lar otomatik enjekte edilir

    @Test
    void shouldPlaceOrder() {
        // @InjectMocks, OrderService'in constructor'ına
        // @Mock ile işaretlenen nesneleri otomatik geçirir
    }
}

@InjectMocks, hedef sınıfın constructor'ına, setter'larına veya field'larına uygun mock'ları otomatik enjekte eder. Constructor injection tercih edilir — bu aynı zamanda Spring'in de önerdiği dependency injection yöntemidir.

when().thenReturn() — Davranış Tanımlama (Stubbing)

@Test
void shouldReturnProductWhenFound() {
    // Arrange — Mock'un davranışını tanımla
    Product laptop = new Product(1L, "Laptop", 999.99);
    when(productRepository.findById(1L))
        .thenReturn(Optional.of(laptop));

    // Act
    Product result = orderService.getProduct(1L);

    // Assert
    assertEquals("Laptop", result.getName());
    assertEquals(999.99, result.getPrice());
}

@Test
void shouldReturnDefaultForUnknownProduct() {
    // Stub tanımlanmamış çağrılar varsayılan değer döner
    // Optional için: Optional.empty()
    // Sayılar için: 0
    // Boolean için: false
    // Object için: null
    when(productRepository.findById(anyLong()))
        .thenReturn(Optional.empty());

    assertThrows(ProductNotFoundException.class,
        () -> orderService.getProduct(999L));
}

Birden fazla çağrıda farklı sonuçlar döndürmek:

@Test
void shouldReturnDifferentResultsOnConsecutiveCalls() {
    when(productRepository.count())
        .thenReturn(0L)    // İlk çağrı
        .thenReturn(1L)    // İkinci çağrı
        .thenReturn(5L);   // Üçüncü ve sonraki çağrılar

    assertEquals(0L, productRepository.count());
    assertEquals(1L, productRepository.count());
    assertEquals(5L, productRepository.count());
    assertEquals(5L, productRepository.count()); // Hep 5L döner
}

verify() — Çağrı Doğrulama

Mock'un belirli metotlarının çağrılıp çağrılmadığını doğrulamak:

@Test
void shouldSendNotificationAfterOrder() {
    Product product = new Product(1L, "Laptop", 999.99);
    when(productRepository.findById(1L))
        .thenReturn(Optional.of(product));

    orderService.placeOrder(1L, 2);

    // Notification gönderildi mi?
    verify(notificationService).sendOrderConfirmation(any(Order.class));

    // Tam olarak 1 kez çağrıldı mı?
    verify(notificationService, times(1))
        .sendOrderConfirmation(any());

    // Hiç çağrılmadı mı?
    verify(notificationService, never())
        .sendOrderCancellation(any());

    // En az / en fazla
    verify(productRepository, atLeastOnce()).findById(anyLong());
    verify(productRepository, atMost(3)).findById(anyLong());
}

ArgumentCaptor — Argüman Yakalama

Bir mock'a geçirilen argümanları yakalayıp incelemek:

@Test
void shouldCaptureOrderDetails() {
    Product product = new Product(1L, "Laptop", 999.99);
    when(productRepository.findById(1L))
        .thenReturn(Optional.of(product));

    orderService.placeOrder(1L, 3);

    // ArgumentCaptor ile geçirilen Order nesnesini yakala
    ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
    verify(notificationService).sendOrderConfirmation(orderCaptor.capture());

    // Yakalanan nesneyi incele
    Order capturedOrder = orderCaptor.getValue();
    assertEquals("Laptop", capturedOrder.getProductName());
    assertEquals(3, capturedOrder.getQuantity());
    assertEquals(2999.97, capturedOrder.getTotalPrice(), 0.01);
}

doThrow() — Exception Fırlatma

void metotlar veya özel durumlar için:

@Test
void shouldHandleRepositoryFailure() {
    // void metotlarda when() yerine doThrow() kullanılır
    doThrow(new DataAccessException("DB error") {})
        .when(productRepository).deleteById(1L);

    assertThrows(ServiceException.class,
        () -> orderService.deleteProduct(1L));
}

@Test
void shouldThrowForInvalidInput() {
    when(productRepository.save(any()))
        .thenThrow(new ConstraintViolationException("Duplicate", null, null));

    assertThrows(DuplicateProductException.class,
        () -> orderService.createProduct(duplicateProduct));
}

Spy — Gerçek Nesne Üzerinde Kısmi Mock

@Test
void shouldUseSpy() {
    // Gerçek listeyi spy ile sar
    List<String> realList = new ArrayList<>();
    List<String> spyList = spy(realList);

    // Gerçek metotlar çalışır
    spyList.add("one");
    spyList.add("two");
    assertEquals(2, spyList.size()); // Gerçek size()

    // Belirli bir metodu override et
    doReturn(100).when(spyList).size();
    assertEquals(100, spyList.size()); // Artık 100 döner

    // Ama eleman ekleme hâlâ gerçek
    spyList.add("three");
    verify(spyList).add("three"); // Çağrı kaydedildi
}

⚠️ Spy kullanırken dikkat: when(spyList.size()) yerine doReturn(100).when(spyList).size() kullanın. İlk formatta gerçek metot önce çağrılır, bu yan etkilere yol açabilir.

Argument Matchers

@Test
void argumentMatcherExamples() {
    when(productRepository.findByName(anyString()))
        .thenReturn(Optional.of(new Product()));

    when(productRepository.findByPriceRange(
        eq(0.0),           // tam eşitlik
        anyDouble()        // herhangi bir double
    )).thenReturn(List.of());

    // Özel matcher
    when(productRepository.findByName(argThat(name ->
        name.length() > 3 && name.startsWith("A")
    ))).thenReturn(Optional.of(new Product()));
}

⚠️ Kural: Bir argümanda matcher kullanıyorsanız, tüm argümanlarda matcher kullanmalısınız. Karışık kullanım derleme hatasına neden olur: eq(value) ile sabit değeri matcher'a dönüştürün.

Mockito, Spring Boot test yazımının vazgeçilmez aracıdır. Mock ile izolasyon, stub ile davranış tanımlama, verify ile çağrı doğrulama ve ArgumentCaptor ile argüman inceleme — bu dört temel mekanizmayı ustaca kullanarak güvenilir birim testler yazabilirsiniz.