Są to obiekty pomocnicze używane w testach jednostkowych, które zastępują prawdziwy obiekt. Pomagają w testowaniu funkcjonalności, które odwołują się do innych klas, czy metod.

W tym artykule spróbuję rozjaśnić różnicę między obiektami z tytułu i wyjasnić do czego każde z nich może posłużyć.

Stub

Jest to obiekt zawierający przykładową implementację kodu, którego zachowanie chcemy przetestować i zwracający określone wartości. Używamy go kiedy chcemy przetestować konkretny stan naszego programu do celów testowych. Kiedy chcemy przetestować inny stan musimy napisać kolejny stub. Jest to zatem dobre rozwiązanie dla prostych funkcji, w których musimy sprawdzić tylko jeden scenariusz.

Przykłady zastosowania:

  • 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)

Przykład

Tworzymy katalog do zarządzania danymi z biblioteki. Nasz program na razie zawiera następujące klasy:

  • Book – odzwierciedla nam uproszczoną klasę odzwierciedlającą książkę
public class Book {
    String name;
    BookStatus bookStatus;

    public Book(String name) {
        this.name = name;
        bookStatus = BookStatus.AVAIBLE;
    }

    public BookStatus getBookStatus() {
        return bookStatus;
    }

    public void setBookStatus(BookStatus bookStatus) {
        this.bookStatus = bookStatus;
    }

    public String getName() {
        return name;
    }
    public boolean isAvailable(){
        if(this.getBookStatus() == BookStatus.AVAIBLE){
            return true;
        }
        return false;
    }

} 
  • LibraryRepository – interfejs, który pozwala nam zaimplementować kod do pobierania danych z katalogu
public interface LibraryRepository {
    List<Book> getAllBooks();
} 
  • LibraryService – klasa, która służy do zwracania nam wybranych wyników posługując się danymi zwracanymi przez z klasy implentującej klasę LibraryRepository
public class LibraryService {
    LibraryRepository libraryRepository;

    public LibraryService(LibraryRepository libraryRepository) {
        this.libraryRepository = libraryRepository;
    }

    public List<Book> getAllAvailableBooks(){
        return libraryRepository.getAllBooks().stream()
                .filter(Book::isAvailable)
                .collect(Collectors.toList());
    }
} 

Próbujemy teraz przetstować metodę getAllAvailableBooks z klasy LibraryService jednak nie posiadam bądź nie chcę korzystać z implementacji i tu potrzebny jest nam stub, który zwróci nam przykładową listę książek. Możemy to zrobić na przykład tworząc osobną klasę:

public class LibraryRepositoryStub implements LibraryRepository{
    @Override
    public List<Book> getAllBooks() {
        Book book1 = new Book("Dr. Dolittle");
        
        Book book2 = new Book("Dzieci z Bullerbyn");
        book2.setBookStatus(BookStatus.RENTED);
        
        Book book3 = new Book("Pippi Pończoszanka");
        
        return Arrays.asList(book1, book2, book3);
    }

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

W teście skorzystamy teraz z naszego stuba:

class LibraryServiceTest {
    @Test
    void getAllActiveAccounts() {
        //given
        LibraryRepository libraryRepositoryStub = new LibraryRepositoryStub();
        LibraryService libraryService = new LibraryService(libraryRepositoryStub);

        //when
        List<Book> booksList = libraryService.getAllAvailableBooks();

        //then
        assertEquals(2, booksList.size());
    }
} 

Czyli przesłaliśmy w stubie z góry określone dane, żeby sprawdzić, czy nasza metoda getAllAvailableBooks oddzieli książki dostępne od innych.

Mock

Jest to obiekt, który w kontrolowany sposób symuluje zachowanie rzeczywistego obiektu. Warto rozważyć jego użycie kiedy rzeczywisty obiekt:

  •  zwraca wyniki, które odnoszą się do stanu obecnego w danym momencie (np. aktualny czas, temperatura)
  • ma stany, które są trudne do wywołania lub zreplikowania (np. błąd sieciowy)
  • jest powolny (np. kompletna baza danych wymagająca inicjalizacji przed testem)
  • jeszcze nie istnieje lub jego zachowanie może się zmienić
  • wymagałby dołączenia informacji i metod przeznaczonych wyłącznie do testów, a nie do realizacji celu, dla którego powstał
Oczekujemy od niego, że zweryfikuje wywołanie odpowiednich metod

Przykład

Powiedzmy, że do naszego poprzedniego przykładu do klasy LibraryService chcemy dodać metodę umożliwiającą użytkownikowi wypożyczenie książki.

public void rentBook(String name){
    Book book = libraryRepository.getByName(name);
    if(book.isAvailable()){
        book.setBookStatus(BookStatus.RENTED);
    } else {
        throw new NoSuchElementException();
    }
} 

Chcemy sprawdzić, czy po wypożyczeniu książki zmieni się jej status, jednak nie mamy dostępu do klasy implementującej LibraryRepository, której metodę wykorzystuje powyższa funkcja. Dlatego stworzymy z niej mock’a

@Test
void rentBookShouldChangeItStatus() {
    //given
    LibraryRepository libraryRepository = mock(LibraryRepository.class);
    LibraryService libraryService = new LibraryService(libraryRepository);
    String bookName = "Dzieci z Bullerbyn";
    Book book = new Book(bookName);

    given(libraryRepository.getByName(bookName)).willReturn(book);

    //when
    libraryService.rentBook(bookName);

    //then
    assertEquals(BookStatus.RENTED, book.getBookStatus());
} 

Kiedy używać mock'a?

  • wyizolowanie zależności w kodzie i wyspecyfikowanie ich zachowania
  • stworzenie zależności, aby sprawdzić interakcje testowanej klasy z nimi

Kiedy nie używać mock'a?

  • do danych testowych oraz obiektów Value
  • do kodu bibliotek

Spy

Obiekt podobny do mock’a, ponieważ jego działanie możemy śledzić i weryfikować, a metody mockować. Różnica między nim, a mockiem polega na tym, że można na nim także wywoływać prawdziwe metody.

Przykład

Powiedzmy, że chcemy przetestować klasę Book i sprawdzić, czy po zmianie BookStatus funkcja IsAvailable zadziała

@Test
void bookShouldBeNotAvailableAfterChangeBookStatusToRented() {
    Book book = spy(new Book("Dzieci z Bullerbyn"));

    given(book.getBookStatus()).willReturn(BookStatus.RENTED);

    assertFalse(book.isAvailable());
} 

Verify

Metoda w bibliotece Mockito służąca do weryfikacja wywołań metod na mock i spy. Czyli umożliwia sprawdzenie, czy stworzony mock/spy wywoła metody obiektu, który udaje.

Przykład

Moglibyśmy użyć tej metody w nieco zmodyfikowanym teście z poprzedniego przykładu, czyli:

@Test
void bookShouldBeNotAvailableAfterChangeBookStatusToRented() {
    Book book = spy(new Book("Dzieci z Bullerbyn"));

    given(book.getBookStatus()).willReturn(BookStatus.RENTED);

    boolean result = book.isAvailable();

    verify(book).getBookStatus();
    assertFalse(result);
} 

Krotność verify

Używając metody verify możemy modyfikować ilość wywołań, czy interakcji za pomocą metod podawanych jako drugi, opcjonalny argument. Przykłady:

  • times(int) – określa ile razy miała zostać wywołana dana metoda podczas sprawdzania danej funkcji
verify(book, times(1)).getBookStatus(); 
  • atMost(int) – określa ile najwięcej razy miałaby być użyta dana funkcja
verify(book, atMost(3)).getBookStatus(); 
  • atLeast(int) – określa ile conajmniej razy miała zostać użyta dana funkcja
verify(book, atLeast(2)).getBookStatus(); 
  • never() – sprawdza, czy nie doszło do interakcji z daną metodą
verify(book, never()).getBookStatus(); 

Istnieją również inne metody używane zamiast verify() sprawdzające, czy nie doszło do interakcji

  • verifyZeroInteractions() – sprawdza, czy nie doszło do żadnej interakcji
verifyZeroInteractions(book); 
  • verifyNoMoreInteractions() – sprawdza, czy mock/spy nie będzie więcej wywoływany
verifyNoMoreInteractions(book); 

Za pomocą klasy InOrder można dodatkowo sprawdzić, czy interakcje zaszły w konkretnej kolejności. Działa ona dla jednego i wielu mocków.

@Test
void bookShouldBeNotAvailableAfterChangeBookStatusToRented() {
    Book book = spy(new Book("Dzieci z Bullerbyn"));

    boolean result = book.isAvailable();

    verify(book, times(1)).getBookStatus();
    assertTrue(result);

    InOrder inOrder = inOrder(book);
    inOrder.verify(book).isAvailable();
    inOrder.verify(book).getBookStatus();
} 

Podsumowanie

W powyższym artykule starałam się jak najlepiej przedstawić różnice pomiędzy obiektami typu stub, mock i spy, a także ich zastosowanie. Tego typu rozwiązania ułatwiają twrzenie testów jednostkowych, a jednocześnie zapobiegają niechcianym skutkom ubocznym,np. modyfikacji lub utracie danych w bazach.