Stub. Spy. Mock.

W testowaniu oprogramowania, zwłaszcza w testach jednostkowych, kluczowym elementem jest izolowanie jednostki kodu od jej zależności. Aby to osiągnąć, programiści często korzystają z narzędzi takich jak spy, stub i mock. Te narzędzia mają różne zastosowania i pomagają w zapewnieniu, że testy są dokładne i szybkie, umożliwiają rejestrację wywołań oraz monitorowanie interakcji.
Poniżej przyjrzymy się, czym są te narzędzia i jak mogą być używane w praktyce z przykładami w języku Java.

Stub:

to pomocniczy obiekt (uproszczona forma obiektu lub metody) używany do symulowania zachowania rzeczywistych obiektów, które testowany kod potrzebuje do działania. Stuby służą głównie do dostarczania z góry ustalonych odpowiedzi na wywołania metod, co pozwala izolować kod testowany od jego zależności i skupić się na logice samego kodu ponieważ są to przykładowe ręczne implementacje danych funkcjonalności, które mogą być wystarczające w małych projektach lub serwisach. Są one pomocne w testowaniu, gdy chcemy zastąpić zewnętrzne zależności (np. wywołania sieciowe, bazy danych) prostszymi odpowiednikami.
Obiekt tego typu nie rejestruje również żadnych informacji o tym, jak został wywołany.

Spy:

w testowaniu jednostkowym, obiekt Spy jest specjalnym rodzajem obiektu testowego, który pozwala zarówno monitorować wywołania metod (jakie metody były wywoływane, z jakimi argumentami, ile razy itp.), jak i opcjonalnie dostarczać predefiniowane odpowiedzi na te wywołania.
Dzięki temu, w odróżnieniu od mocków, które całkowicie zastępują funkcje lub obiekty, obiekty typu spy monitorują prawdziwe obiekty lub metody.
Obiekty tego typu są używane, gdy chcemy zbadać, jak testowany kod wchodzi w interakcje z jego zależnościami, a jednocześnie umożliwić tym zależnościom zachowanie się w sposób zbliżony do ich rzeczywistych implementacji.
Wykorzystanie obiektów typu spy jest również pomocne kiedy szukamy alternatywy dla wywołania prawdziwej metody na obiekcie typu mock.
Jest to wrapper opakowujący obiekty innych klas.

Mock:

to obiekt testowy, który jest zarówno stub’em, jak i spy’em. Oznacza to, że może zwracać z góry ustalone odpowiedzi oraz rejestrować szczegóły wywołań metod.
Wykorzystanie mocków w kodzie umożliwia biblioteka Mockito. Są one bardzo elastyczne i pozwalają na szczegółową kontrolę interakcji między testowanym kodem a jego zależnościami.
Jest to rodzaj obiektu testowego, który symuluje zachowanie rzeczywistego obiektu w kontrolowany sposób. Jest bardziej zaawansowany niż stub, który pozwala na pełną kontrolę nad tym, jak zachowuje się zastępowany obiekt lub metoda.
Główne wykorzystanie: Mocki pozwalają na dokładne zdefiniowanie, jakie odpowiedzi mają być zwracane na konkretne wywołania metod.

Na potrzeby prezentacji przykładów z zastosowaniem przedstawionych obiektów wykorzystana zostanie implementacja funkcjonalności pobierania aktywnych kont klientów z bazy danych.

//klasa reprezentująca adres klienta podany podczas zakładania       //konta
public class Address {
 
    private String street;
    private String number;
 
    public Address(String street, String number) {
        this.street = street;
        this.number = number;
    }
}

//klasa założonego konta klienta
public class Account {
 
    private boolean active;
    private Address defaultDeliveryAddress;
 
    public Account(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
        if(defaultDeliveryAddress !=null) {
            activate();
        } else {
            this.active = false;
        }
    }
 
    public Account() {
        this.active = false;
    }
 
    public void activate() {
        this.active = true;
    }
 
    public boolean isActive() {
        return this.active;
    }
}

//klasa serwisowa do obsługi kont
public class AccountService {
 
    private AccountRepository accountRepository;
 
    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
 
    List<Account> getAllActiveAccounts() {
        return accountRepository.getAllAccounts().stream()
                .filter(Account::isActive)
                .collect(Collectors.toList());
    }
 
    List<String> findByName(String name) {
        return accountRepository.getByName(name);
    }
}

//interfejs repozytorium kont w serwisie
public interface AccountRepository {
 
    List<Account> getAllAccounts();
    List<String> getByName(String name);
}

Testy jednostkowe z wykorzystaniem obiektu typu Stub będą polegać na stubowaniu intefejsu AccountRepository to znaczy nadaniu sygnaturom tych metod własnych implementacji ponieważ zakładamy, że nie mamy dostepu do oryginalnych implementacji tych metod:

public class AccountRepositoryStub implements AccountRepository {
    @Override
    public List<Account> getAllAccounts() {
        Address address1 = new Address("Kościelna", "18");
        Account account1 = new Account(address1);
 
        Account account2 = new Account(); //instancja bez //podanego adresu w konstruktorze, oznaczona bedzie jako //nieaktywna

        Address address2 = new Address("Koniecpolska", "4");
        Account account3 = new Account(address2);
 
        return Arrays.asList(account1,account2,account3);
    }
 
    @Override
    public List<String> getByName(String name) {
        return null;
    }
}

Test jednostkowy metody zwracającej wszystkie aktywne konta:

@Test
void getAllActiveAccounts() {
 
    //given
    AccountRepositoryStub accountRepositoryStub = new             AccountRepositoryStub();
    AccountService accountService = new AccountService(accountRepositoryStub);
 
    //when
    List<Account> activeAccounts = accountService.getAllActiveAccounts();
 
    //then
    assertThat(activeAccounts.size(), is(2));
}

Stuby sprawdzają się dobrze przy projektach niewielkich rozmiarów. W przypadku rozszerzenia kodu i stubowanego interfejsu o nowe klasy i metody zachodzi konieczność ich implementacji w klasie stubowej.

Za pomocą Mocka zasymulowane zostanie zachowanie rzeczywistego obiektu – zwrócenie listy (pustej) kont, które nie są aktywne:

@Test
void getNoActiveAccounts() {
 
    //given
    AccountRepository accountRepository = mock(AccountRepository.class);
    AccountService accountService = new AccountService(accountRepository);
    given(accountRepository.getAllAccounts()).willReturn(Collections.emptyList());
 
    //when
    List<Account> accountList = accountService.getAllActiveAccounts();
 
    //then
    assertThat(accountList, hasSize(0));
}

Mocki mogą być tworzone dynamicznie, w czasie run time’u aplikacji przez co zapewniają większą elastyczność niż Stuby.

Obiekty testowe typu Spy mogą zachowywać się jak rzeczywiste obiekty, wykonując rzeczywiste metody, chyba że zostaną zmodyfikowane do zwracania konkretnych odpowiedzi. Taka konstrukcja to połączenie cech prawdziwych obiektów oraz mocków przez co nazywane są również partial mockami.

Nowa klasa, która będzie testowana:

public class Meal {
    private int price;
    private int quantity;
 
    public Meal() {
    }
 
    public int getQuantity() {
        return quantity;
    }
 
    public int getPrice() {
        return price;
    }
 
    int sumPrice() {
        return getPrice() * getQuantity();
    }
}

Test jednostkowy z wykorzystaniem wrappera Spy:

@Test
void testMealSumPriceWithSpy() {
 
    //given
    Meal meal = spy(Meal.class);
    given(meal.getPrice()).willReturn(15);
    given(meal.getQuantity()).willReturn(3);
 
    //when
    int result = meal.sumPrice();
 
    //then
    then(meal).should().getPrice();
    then(meal).should().getQuantity();
    assertThat(result, equalTo(45));
}

Kiedy chcę korzystać z prawdziwego zachowania części metod w obiekcie,
a zachowanie innych metod chcę mockować lub jeśli chcę mieć możliwość weryfikacji wywołania metod zachowując jednak ich prawdziwe zachowanie to powinienem korzystać z obiektu typu Spy.
To jest wrapper – opakowuje obiekt danej klasy, którego działanie można śledzić i monitorować jak mock.

,

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *