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.

