Stub vs Mock vs Spy

spyCoverPhoto

W poniższym artykule na przeciwko siebie w ringu staną trzy obiekty pomocnicze używane w testach jednostkowych – stub, mock i spy. Dowiesz się dlaczego używamy tego typu obiektów, zobaczysz ich przykłady oraz poznasz różnice pomiędzy nimi.

Załóżmy, że współtworzymy sklep internetowy. Do korzystania z serwisu użytkownicy będą musieli utworzyć konto. Jeśli podczas jego tworzenia podadzą adres – konto stanie się aktywne. Chcielibyśmy również mieć możliwość pobierania kont klientów z bazy danych, np. w celu wysłania newslettera. Utworzymy także klasę serwisową, służącą do pobierania i filtrowania kont z repozytorium tak, by zwracać wyłącznie konta aktywne. Dysponujemy zatem klasami Address, Account, AccountService oraz interfejsem AccountRepository. Zakładamy, że nie mamy dostępu do kodu, który implementuje wspomniany interfejs:

public class Address {

    private String street;
    private String number;

    public Address(String street, String number) {
        this.street = street;
        this.number = number;
    }
}
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;
    }
}
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);
    }
}
public interface AccountRepository {

    List<Account> getAllAccounts();
    List<String> getByName(String name);
}

Rozważmy jak przetestowalibyśmy nasz kod przy użyciu tytułowych obiektów.

Stub

Stub to obiekt, który zawiera przykładową implementację imitującą działanie tej właściwej. Przykładami jego użycia mogą być następujące sytuacje:

  • nie mamy dostępu do prawdziwej metody zwracającej dane
  • nie chcemy angażować obiektów, które zwróciłyby prawdziwe dane, co mogłoby mieć niekorzystne skutki uboczne (np. modyfikacja danych w bazie danych)

Chcąc przetestować metodę zwracającą wszystkie aktywne konta będziemy potrzebować danych. Tak jak wspomniałem we wstępnie – nie mamy do nich dostępu. Z pomocą przychodzi właśnie klasa stubowa, która zwróci przykładowe dane:

public class AccountRepositoryStub implements AccountRepository{
    @Override
    public List<Account> getAllAccounts() {
        Address address1 = new Address("Herberta", "33");
        Account account1 = new Account(address1);

        Account account2 = new Account();
        Address address2 = new Address("Piłsudskiego", "12/8");

        Account account3 = new Account(address2);

        return Arrays.asList(account1,account2,account3);
    }

    @Override
    public List<String> getByName(String name) {
        return null;
    }
}

W teście skorzystamy z powyższej klasy:

    @Test
    void getAllActiveAccounts() {

        //given
        AccountRepository accountRepositoryStub = new AccountRepositoryStub();
        AccountService accountService = new AccountService(accountRepositoryStub);

        //when
        List<Account> activeAccounts = accountService.getAllActiveAccounts();

        //then
        assertThat(activeAccounts.size(), is(2));
    }

Obiekty stub działają dobrze dla prostych metod i przykładów, jednakże przy większej liczbie warunków testowych oraz przy możliwym rozroście interfejsów nie sprawdzają się. Mogą urosnąć do sporych rozmiarów i być ciężkie w utrzymaniu.

Mock

Mock to obiekt symulujący działanie rzeczywistego obiektu. Pozwala określić jakich interakcji spodziewamy się w trakcie testów, a następnie zweryfikować, czy nastąpiły. Z wykorzystaniem mocka przetestujmy sytuację, w której baza danych nie zwróci żadnych danych:

    @Test
    void getNoActiveAccounts() {

        //given
        AccountRepository accountRepository = mock(AccountRepository.class);
        AccountService accountService = new AccountService(accountRepository);
        given(accountRepository.getAllAccounts()).willReturn(Collections.emptyList());

        //when
        List<Account> activeAccounts = accountService.getAllActiveAccounts();

        //then
        assertThat(activeAccounts.size(), is(0));
    }

Do utworzenia mocka skorzystaliśmy z biblioteki Mockito 2, a konkretniej funkcji mock(), przyjmującej jako argument nazwę klasy, którą chcemy symulować. Metody given() i willReturn() zasymulowały takie działanie, w którym jeżeli na naszym mocku wywołamy metodę getAllAcounts(), to zostanie zwrócona pusta lista.

W porównaniu do stubów, mocki być tworzone dynamicznie w czasie działania kodu oraz zapewniają większą elastyczność. Dają też znacznie więcej funkcjonalności, takich jak chociażby weryfikacja wywołań metod (czy zostały wywołane, ile razy, w jakiej kolejności, z jakimi parametrami itp.).

Spy

Spy to obiekt hybrydowy – mieszanka obiektów prawdziwych oraz mocków. Jego działanie można śledzić i weryfikować. Wyróżnia go fakt, że działanie jego wybranych metod można mockować. Może on zatem być częściowo mockiem i częściowo normalnym obiektem.

Do omówienia jego działania posłużymy się nowym przykładem. Załóżmy, że posiadamy klasę Meal:

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();
    }
}

Przy użyciu obiektu spy chcielibyśmy zweryfikować metodę sumującą koszt:

    @Test
    void testTotalMealPrice() {

        //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));
    }

W celu utworzeniu obiektu posłużyliśmy się wrapperem spy(). Można zauważyć, że zarówno getPrice() i getQuantity() mają zaprogramowane działanie, natomiast do zmiennej result przypisaliśmy wynik zwrócony przez prawdziwą metodę. Dodatkowo w sekcji „then” możliwe było zweryfikowanie wywołań metod, dzięki skorzystaniu z konstrukcji then().should().

Obiektów spy używamy, gdy zależy nam na korzystaniu z prawdziwego zachowania części metod w obiekcie lub gdy chcemy mieć możliwość weryfikacji wywołań metod zachowując ich prawdziwe zachowanie.

Podsumowanie

Po przeczytaniu tego artykułu wiesz czym jest stub, mock i spy, potrafisz wskazać różnice między poszczególnymi obiektami oraz w jakich okolicznościach je stosować. Korzystanie z tego typu rozwiązań znacznie ułatwi tworzenie testów jednostkowych, a także zapobiegnie niechcianym skutkom ubocznym, np. utracie lub modyfikacji danych w bazach.

Cały kod z wpisu znajduje się na moim Githubie.

Zostaw komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *