Stub, mock i spy
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ł
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.