Zawaansowane mockowanie z Mockito

Tomasz Głuszak

Zaktualizowaliśmy ten tekst dla Ciebie!
Data aktualizacji: 30.12.2024
Autor aktualizacji: Krzysztof Kramarz

Cel testów jednostkowych

Celem testów jednostkowych jest testowanie małych fragmentów kodu niezależnie, w izolacji od jakichkolwiek zależności. W wielu przypadkach, aby utrzymać taki poziom separacji, przydają się narzędzia takie jak Mockito. Mockito to framework do mockowania, biblioteka Java, która umożliwia symulowanie wywołań na obiektach zależnych zamiast wywoływania ich rzeczywistych odpowiedników. Obiekt mockowany zwraca dane zastępcze odpowiadające wprowadzonym danym testowym.

Prosty przypadek

Załóżmy, że mamy prostą klasę o nazwie CustomerService z metodą addCustomer(). Metoda w zamyśle służy do dodania nowego klienta. Przyjmuje ona jako parametr encję Customer. Usługa ta korzysta z CustomerDao, który łączy się z bazą danych. Podczas testowania CustomerService nie chcemy używać rzeczywistego połączenia z bazą danych, dlatego zamokujemy CustomerDao.

public class CustomerService {
    private CustomerDao customerDao;

    public boolean addCustomer(Customer customer) {
        if (customerDao.getById(customer.getId()) != null) {
            return false;
        }
        return customerDao.save(customer);
    }
}
Test JUnit dla przypadku, w którym klient już istnieje, wygląda następująco:
@RunWith(MockitoJUnitRunner.class)
public class CustomerServiceTest {
    @InjectMocks
    private CustomerService underTest;
    @Mock
    private CustomerDao customerDao;
    private long customerId = 28;
    private Customer customer = new Customer(customerId);

    @Test
    public void addCustomerWhenExists() {
        when(customerDao.getById(customerId)).thenReturn(customer);
        boolean result = underTest.addCustomer(customer);
        assertFalse(result);
        verify(customerDao, never()).save(customer);
    }
}

Zapis when(…).thenReturn(…) oznacza, że gdy metoda getById() zostanie wywołana z parametrem customerId=28, obiekt customer zostanie zwrócony, symulując scenariusz, w którym klient o podanym ID już istnieje w bazie danych.

W ostatniej linii weryfikujemy, że metoda save nigdy nie została wywołana.

Weryfikacja argumentów metod mockowanych

W niektórych przypadkach musimy sprawdzić więcej informacji o argumentach przekazywanych do metod, które są mockowane, szczególnie gdy argumenty są przekazywane między mockami lub modyfikowane w trakcie wykonywania.

Rozszerzmy nasz przykład, dodając CustomerNumberGenerator, który wygeneruje numer klienta i ustawi go w obiekcie klienta przed zapisaniem encji.

public class CustomerService {
    private CustomerDao customerDao;
    private CustomerNumberGenerator customerNumberGenerator;

    public boolean addCustomer(Customer customer) {
        if (customerDao.getById(customer.getId()) != null) {
            return false;
        }
        customerNumberGenerator.generate(customer);
        return customerDao.save(customer);
    }
}

@FunctionalInterface
public interface CustomerNumberGenerator {
    void generate(Customer customer);
}

W kolejnym teście jednostkowym chcemy pozwolić customerNumberGenerator na wywołanie metody generate() w implementacji dostarczonej w teście i sprawdzić argument przekazany do customerDao.save().

@Captor
private ArgumentCaptor<Customer> customerCaptor;
private String customerNumber = "STC3288";

@Test
public void addCustomerCheckCustomerNumber() {
  underTest.setCustomerNumberGenerator(customer -> customer.setCustomerNumber(customerNumber));
    when(customerDao.getById(customerId)).thenReturn(null);
    when(customerDao.save(customer)).thenReturn(true);

    boolean result = underTest.addCustomer(customer);

    assertTrue(result);
    verify(customerDao).save(customerCaptor.capture());
    assertEquals(customerNumber, customerCaptor.getValue().getCustomerNumber());
}

Wyjaśnienie:

  1. ArgumentCaptor: Używamy obiektu ArgumentCaptor<Customer>, aby przechwycić argument przekazany do mocka.
  2. Implementacja testowa: Na początku dostarczamy naszą testową implementację CustomerNumberGenerator do CustomerService za pomocą wyrażenia lambda.
  3. Weryfikacja i przechwycenie argumentu:
    • Podczas wywołania verify() na customerDao, używamy metody capture() obiektu captor.
    • Następnie, gdy wywołujemy getValue(), mamy dostęp do obiektu przekazanego jako argument do mockowanej metody.
  4. Sprawdzanie właściwości: Na końcu sprawdzamy, czy właściwość customerNumber została prawidłowo ustawiona.

Używanie Answers do złożonych mocków

Czasami metoda może wykonywać bardziej złożone operacje niż tylko ustawianie lub dodawanie wartości. W takich sytuacjach możemy użyć klasy Answer z Mockito, aby dodać potrzebne zachowanie. Zamiast ustawiać niestandardowy CustomerNumberGenerator w teście, skorzystamy z Mockito i metody doAnswer().

@Mock
private CustomerNumberGenerator customerNumberGenerator;

@Test
public void addCustomerWithCustomerNumber() {
    when(customerDao.getById(customerId)).thenReturn(null);
    when(customerDao.save(customer)).thenReturn(true);

    doAnswer(invocation -> {
        Customer customer = invocation.getArgument(0);
        customer.setCustomerNumber(customerNumber);
        return null;
    }).when(customerNumberGenerator).generate(customer);

    boolean result = underTest.addCustomer(customer);

    assertTrue(result);
    verify(customerDao).save(customerCaptor.capture());
    assertEquals(customerNumber, customerCaptor.getValue().getCustomerNumber());
}

Wyjaśnienie:

  1. Dlaczego doAnswer():
    • Używamy doAnswer(…).when() zamiast when(…).thenAnswer(…), ponieważ metoda generate() zwraca void. when(…).thenAnswer(…) nie obsługuje takich metod.
  2. Zachowanie mocka w doAnswer():
    • Wewnątrz wyrażenia lambda uzyskujemy dostęp do argumentu przekazanego do metody generate() za pomocą invocation.getArgument(0).
    • Następnie ustawiamy customerNumber w obiekcie customer.
  3. Złożone operacje:
    • W prostym przypadku ustawiamy właściwość, tak jak w poprzednim przykładzie, ale w bardziej złożonych przypadkach można wykonywać dodatkowe operacje, np. sprawdzanie argumentów lub inne elementy logiki.
  4. Weryfikacja i asercje:
    • Podobnie jak wcześniej, weryfikujemy wywołania i sprawdzamy, czy właściwość customerNumber została ustawiona prawidłowo.

Korzyść:

Dzięki doAnswer() można dynamicznie symulować złożone zachowania, które trudno byłoby osiągnąć za pomocą prostych when(…).thenReturn(…).

Weryfikacja wywołań rzeczywistych metod

W niektórych przypadkach chcemy wykonać rzeczywiste metody i zweryfikować ich wywołanie. W takich sytuacjach Mockito udostępnia adnotację @Spy.

W poniższym przykładzie zakładamy, że istnieje jakaś implementacja CustomerNumberGenerator, a my chcemy wywołać rzeczywistą metodę generate() i ją zweryfikować.

@Spy
private CustomerNumberGeneratorImpl customerNumberGenerator;

@Test
public void addCustomerRealGenerator() {
    when(customerDao.getById(customerId)).thenReturn(null);
    when(customerDao.save(customer)).thenReturn(true);

    boolean result = underTest.addCustomer(customer);

    assertTrue(result);
    verify(customerDao).save(customer);
    verify(customerNumberGenerator).generate(customer);
}

Wyjaśnienie:

  1. Adnotacja @Spy:
    • Dzięki @Spy rzeczywista implementacja CustomerNumberGeneratorImpl jest automatycznie wstrzykiwana do obiektu underTest (tak samo jak przy użyciu @Mock).
    • Oznacza to, że rzeczywiste metody klasy CustomerNumberGeneratorImpl będą wywoływane.
  2. Wywołanie metody generate():
    • W trakcie działania testu metoda generate() jest rzeczywiście wywoływana.
  3. Weryfikacja wywołania:
    • Ostatnia linia testu (verify(…).generate(customer)) sprawdza, czy metoda generate() została wywołana na rzeczywistej implementacji obiektu.

Zastosowanie:

Korzystanie z @Spy jest przydatne, gdy chcemy przetestować rzeczywiste metody i jednocześnie zweryfikować ich zachowanie, np. upewniając się, że logika w implementacji została poprawnie wywołana.

Podsumowanie
Mockito to łatwe w użyciu, a jednocześnie potężne narzędzie, które upraszcza testowanie jednostkowe, szczególnie w kontekście separacji zależności. Świetnie sprawdza się w prostych przypadkach mockowania, takich jak „zwróć coś, gdy wywołana zostanie jakaś metoda”, ale ujawnia swój pełny potencjał w bardziej złożonych scenariuszach, takich jak weryfikacja argumentów przekazywanych do metod, obsługa metod void czy weryfikacja wywołań rzeczywistych metod.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami