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.