Mocks, stubs i spy w testach jednostkowych opartych na Mockito

Tomasz Niegowski

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.

Bibliografia

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami