Mocks, stubs i spy w testach jednostkowych opartych na Mockito
Wprowadzenie do mockowania
Podczas pisania testów jednostkowych prawie zawsze istnieje potrzeba interakcji z obiektami w testowanym kodzie. Na pierwszy rzut oka wydaje się, że najłatwiejszym sposobem zarządzania nimi jest po prostu utworzenie nowego rzeczywistego obiektu i zainicjowanie jego wymaganych pól za pomocą konstruktora, buildera lub setterów. Jednak często nie jest możliwe zrobienie tego w prosty sposób, w rozsądnym czasie lub biorąc pod uwagę bezpieczeństwo. Na szczęście w testach jednostkowych istnieją powszechnie znane metody radzenia sobie ze wszystkimi tymi typami obiektów i zostaną one przedstawione w tym artykule.
Klasy przedstawione poniżej zostaną wykorzystane w dalszych przykładach:
Person:
public class Person {
private String name;
private String surname;
private int age;
public Person(String name, String surname, int age) {
this.name = name;
this.surname = surname;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
PeopleRepository:
public interface PeopleRepository {
List<Person> getAllPeople();
List<Person> getByName(String name);
}
PersonService:
public class PersonService {
private PeopleRepository peopleRepository;
public PersonService(PeopleRepository peopleRepository) {
this.peopleRepository = peopleRepository;
}
List<Person> getAdults() {
return peopleRepository.getAllPeople().stream()
.filter(person -> person.getAge() >= 18)
.collect(Collectors.toList());
}
List<Person> findByName(String name) {
return peopleRepository.getByName(name);
}
}
Stubs
Stub jest obiektem z przykładową implementacją, która naśladuje prawdziwą, ale z minimalną liczbą metod wymaganych do testu. Używamy go, aby uniknąć dostępu do prawdziwych danych. Stuby zawsze zwracają predefiniowane dane wyjściowe, niezależnie od danych wejściowych.
Stub najlepiej jest stosować, gdy:
- nie ma dostępu do metody zwracającej dane
- dostęp do rzeczywistego obiektu może mieć skutki uboczne (takie jak modyfikacja danych w bazie danych)
Stuby najlepiej sprawdzają się w przypadku prostych metod. W przypadku testowania dużych, skomplikowanych obiektów stuby mogą być trudne w utrzymaniu.
Zakładając, że chcemy przetestować klasę PersonService, ale nie chcemy operować na prawdziwym PeopleRepository. W takiej sytuacji utworzenie stuba wydaje się być dobrym pomysłem:
public class PeopleRepositoryStub implements PeopleRepository{
@Override
public List<Person> getAllPeople() {
Person person1 = new Person("John", "Doe", 35);
Person person2 = new Person("Joseph", "Dendy", 43);
return List.of(person1, person2);
}
@Override
public List<Person> getByName(String name) {
return null;
}
}
Przykładowy test:
@Test
void getAllPeople() {
//given
PeopleRepository peopleRepositoryStub = new PeopleRepositoryStub();
PersonService personService = new PersonService(peopleRepositoryStub);
//when
List<Person> people = personService.getAdults();
//then
assertThat(people.size(), is(2));
}
Mocks
Mock jest „pustym” obiektem, który symuluje obiekt rzeczywisty. Nie jest możliwe wywołanie metody mockowanego obiektu, dopóki nie zostanie dokładnie określone, co ta metoda powinna zwrócić. Mocki są bardzo przydatne i popularne, ponieważ zapewniają dużą elastyczność i dodatkowe funkcje (np. możliwość weryfikacji liczby wywołań metod, parametry wywołań).
Przetestujmy przypadek, gdy baza danych nie zwróci żadnych danych:
@Test
void getEmptyPeopleList() {
//given
PeopleRepository peopleRepository = mock(PeopleRepository.class);
PersonService personService = new PersonService(peopleRepository);
given(peopleRepository.getAllPeople()).willReturn(Collections.emptyList());
//when
List<Person> people = personService.getAdults();
//then
assertThat(people.size(), is(0));
}
Użyliśmy biblioteki Mockito do stworzenia mocka PeopleRepository, przekazaliśmy go jako parametr konstruktora PersonService i wstępnie zdefiniowaliśmy odpowiedź na wywołanie metody getAllPeople. Jak widzimy na przedstawionym przykładzie, użycie mocka jest dość łatwe i elastyczne.
Spies
Spy jest obiektem mieszanym, składającym się z obiektu rzeczywistego i pozorowanego. Zachowuje się jak prawdziwy obiekt, ale zachowanie określonych metod może być mockowane. Spies są przydatni, gdy istnieje klasa z wieloma metodami i istnieje potrzeba mockowania tylko części z nich.
Przykładowy test:
@Test
void getPersonNameAndAge() {
//given
Person realPerson = new Person("John", "Doe", 35);
Person spyPerson = spy(realPerson);
given(spyPerson.getAge()).willReturn(20);
//when
String actualName = spyPerson.getName();
int actualAge = spyPerson.getAge();
//then
assertThat(actualName, is("John"));
assertThat(actualAge, is(20));
}
Jak widzimy, został utworzony prawdziwy obiekt Person i spy oparty na prawdziwym. Metoda GetName nie została zmodyfikowana, więc oczekujemy danych wyjściowych z prawdziwego obiektu, ale jednocześnie oczekujemy zmodyfikowanego wieku. Udało nam się zachować zachowanie jednej prawdziwej metody i zmienić pozostałe.
Podsumowanie
W artykule przedstawiono i omówiono trzy rodzaje podstawowych zaślepek testów jednostkowych. Teraz powinieneś znać ich charakterystykę i różnice między nimi, a także wiedzieć, kiedy używać każdego z nich.