Stub / Mock / Spy

STUB, MOCK, SPY 

W programowaniu komputerowym i informatyce programiści stosują technikę zwaną zautomatyzowanym testowaniem jednostkowym w celu poprawy jakości oprogramowania. W celu zwiększenia izolacji i niezależności testu jednostkowego często stosuje się tzw. duble testowe (z ang. „test doubles”), które tak naprawdę „udają” w działaniu obiekty zależne, lecz nimi nie są. Przykładem tego typu dubla testowego może być obiekt udający odczyt informacji z bazy danych, potrzebnych do przetestowania logiki biznesowej naszego kodu.

Generalnie możemy wyróżnić 5 typów dubli testowych:

• Dummy (atrapa) – obiekt, który nigdy nie jest tak naprawdę używany, potrzebny jest tylko po to, by uzupełnić listę parametrów metody;

• Fake (podróbka/imitacja) – stosowany jako prostsza implementacja, m.in. używanie w testach bazy danych w pamięci zamiast dostępu do rzeczywistej bazy danych.

• Stub (namiastka) – Stuby to obiekty, które przechowują predefiniowane dane i używają ich do udzielania odpowiedzi podczas testów. Innymi słowy, stub to obiekt, który przypomina prawdziwy obiekt z minimalną liczbą metod potrzebnych do testu. Stuby są używane, gdy nie chcemy używać obiektów, które dawałyby odpowiedź z prawdziwymi danymi. Jest określany jako najlżejsza i najbardziej statyczna wersja dubla testowego. Stuba tworzy się poprzez rozszerzenie prawdziwego obiektu lub interfejsu oraz zaimplementowanie niezbędnych metod zgodnie z wymaganiami scenariuszatestowego. Stub jest zorientowany na stan i jego zachowanie jest statyczne, czyli znane na etapie kompilacji.

• Mock (drwina/kpina) – obiekt, który jest instruowany w jaki sposób ma wyglądać jego interakcja z testem: czy ma zwracać konkretne wartości, czy może ma rzucić wyjątek. Następnie, w fazie weryfikacji, sprawdzamy czy mock wchodził w interakcje z oczekiwanymi obiektami przez odpowiednią długość czasu czy odpowiednią ilość razy. Główną funkcją używania mocków jest to, że daje to pełną kontrolę nad zachowaniem obiektów mock. Są tworzone głównie przy użyciu biblioteki lub frameworka do mockowania, takiego jak Mockito, JMock i EasyMock. Służy do testowania dużego zestawu testów, w których odcinki nie są wystarczające. Mock jest zorientowany na zachowanie i można to zachowanie modyfikować w czasie uruchamiania testu (runtime) – jest dynamiczne.

• Spy (szpieg) – Szpiedzy są znani jako częściowo pozorowane obiekty. Oznacza to, że szpieg tworzy częściowy obiekt lub pół atrapę rzeczywistego obiektu poprzez szpiegowanie prawdziwych metod. Szpiegując, prawdziwy obiekt pozostaje niezmieniony, a my po prostu śledzimy pewne specyficzne metody. Innymi słowy, bierzemy istniejący (rzeczywisty) obiekt i zastępujemy go lub szpiegujemy tylko niektóre z jego metod. Szpiedzy są przydatni, gdy mamy ogromną klasę pełną metod i chcemy z pewnych metod kpić. W tym scenariuszu powinniśmy raczej używać szpiegów niż kpiny i namiastki. Szpieg wywołuje rzeczywiste metody, jeśli metody nie są namiastkami.

W tym artykule chciałbym zwrócić szczególna uwagę na duble testowe typu: Stub, Mock, oraz Spy.

Przeanalizujmy poniższy przykład – Kiedy pojawi się nowy klient z datą rezerwacji zawierającą rok 2022 to chcemy tą rezerwację zaakceptować, w innym wypadku, nie będziemy akceptowali rezerwacji z powodu braku możliwosci realizacji.

Przgotujmy najpierw klasy bazowe: Klient, Rezerwacja, SerwisRezerwacji oraz interfejs RepozytoriumRezerwacji:

public class Klient {

private String imie;
private String data;

public Klient(){}

public Klient(String imie, String data) {
this.imie = imie;
this.data = data;
}

public String pobierzImie(){
return imie;
}
public String pobierzDate() {
return data;
}
public String pobierzImieIDate() {
return pobierzImie() + " " + pobierzDate();
}
}
public class Rezerwacja {

private boolean akceptacja;

public Rezerwacja(Klient rezerwacjaKlienta) {

if(rezerwacjaKlienta.pobierzDate().contains("2022")){
akceptujRezerwacje();
} else {
this.akceptacja = false;
}
}

private void akceptujRezerwacje(){
this.akceptacja = true;
}

public boolean czyRezerwacjaJestZaakceptowana(){
return this.akceptacja;
}
}
public interface RepozytoriumRezerwacji {
List<Rezerwacja> pobierzWszystkieRezerwacje();
}
public class SerwisRezerwacji {

private final RepozytoriumRezerwacji repozytoriumRezerwacji;

public SerwisRezerwacji (RepozytoriumRezerwacji repozytoriumRezerwacji){
this.repozytoriumRezerwacji = repozytoriumRezerwacji;
}

List<Rezerwacja> pobierzeWszystkieZaakceptowaneRezerwacje(){
return repozytoriumRezerwacji.pobierzWszystkieRezerwacje().stream()
.filter(Rezerwacja::czyRezerwacjaJestZaakceptowana)
.collect(Collectors.toList());
}
}

STUB

Najpierw rozważmy jak przetestowalibyśmy nasz kod przy użyciu dubla typu Stub. Chcąc przetestować metodę zwracającą wszystkie zakceptowane rezerwacje będziemy potrzebować danych. Ponieważ nie mamy do nich dostepu, możemy wykorzystać właśnie dubla typu Stub, który przekaże przykładowe dane do weryfikacji:

public class RepozytoriumRezerwacjiStub implements RepozytoriumRezerwacji{

@Override
public List<Rezerwacja> pobierzWszystkieRezerwacje() {

Klient klient1 = new Klient("Adam Bodziak", "2022-02-11");
Rezerwacja rezerwacja1 = new Rezerwacja(klient1);

Klient klient2 = new Klient("Kasia Lubi", "2022-01-29");
Rezerwacja rezerwacja2 = new Rezerwacja(klient2);

Klient klient3 = new Klient("Marysia Motyka", "2021-12-25");
Rezerwacja rezerwacja3 = new Rezerwacja(klient3);

return Arrays.asList(rezerwacja1, rezerwacja2, rezerwacja3);
}
}

Dzięki temu, możemy utworzyć klasę testową z wykorzystaniem powyższej klasy RepozytoriumRezerwacjiStub. Klasa testowa wykorzysta asercje do porównania statycznie ustawionych wartości względem oczekiwanego rezultatu – w naszym przypadku jest to rozmiar listy.

public class SerwisRezerwacjiStubTest {

@Test
void pobierzWszystkieZakceptowaneRezerwacje(){

//given
RepozytoriumRezerwacji repozytoriumRezerwacjiStub = new RepozytoriumRezerwacjiStub();
SerwisRezerwacji serwisRezerwacji = new SerwisRezerwacji(repozytoriumRezerwacjiStub);

//when
List<Rezerwacja> listaRezerwacji = serwisRezerwacji.pobierzeWszystkieZaakceptowaneRezerwacje();

//then
assertThat(listaRezerwacji, hasSize(2));
}
}

Mieliśmy tutaj wstępnie przygotowaną klasę typu Stub która użyła metodę z interfejsu do ustawienia danych wykorzystanych następnie podczas testu.

MOCK

Popatrzmy teraz, jak można zrealizowac to samo zadanie przy pomocy dubla testowego typu mock() z biblioteki Mockito:

public class SerwisRezerwacjiMockTest {
@Test
void pobierzeWszystkieZaakceptowaneRezerwacje(){

//given
List<Rezerwacja> rezerwacje = przygotujDaneRezerwacji();
RepozytoriumRezerwacji repozytoriumRezerwacjiMock = mock(RepozytoriumRezerwacji.class);
SerwisRezerwacji serwisRezerwacji = new SerwisRezerwacji(repozytoriumRezerwacjiMock);
given(repozytoriumRezerwacjiMock.pobierzWszystkieRezerwacje()).willReturn(rezerwacje);

//when
List<Rezerwacja> rezerwacjeZaakceptowane = serwisRezerwacji.pobierzeWszystkieZaakceptowaneRezerwacje();

//then
assertThat(rezerwacjeZaakceptowane, hasSize(2));

}

private List<Rezerwacja> przygotujDaneRezerwacji(){

Klient klient1 = new Klient("Adam Bodziak", "2022-02-11");
Rezerwacja rezerwacja1 = new Rezerwacja(klient1);

Klient klient2 = new Klient("Kasia Lubi", "2022-01-05");
Rezerwacja rezerwacja2 = new Rezerwacja(klient2);

Klient klient3 = new Klient("Marysia Motyka", "2021-12-25");
Rezerwacja rezerwacja3 = new Rezerwacja(klient3);

return Arrays.asList(rezerwacja1, rezerwacja2, rezerwacja3);
}
}

Biblioteka Mockito umożliwia mockowanie klas podobnie jak interfejsów oraz pozwala tworzyć własne argumenty porównujące (matchers). Można również do tego celu użyć np. biblioteki Hamcrest. W porównaniu do stubów, mocki moga być tworzone dynamicznie w czasie działania kodu oraz zapewniają większą elastyczność.

SPY

Na koniec chciałbym pokazać jak możemy używać dubla typu Spy przykładowo na wspomnianej wcześniej klasie Klient:

public class SerwisRezerwacjiSpyTest {

@Test
void testySpy() {

//given
Klient klient = spy(Klient.class);
given(klient.pobierzDate()).willReturn("2022-01-11");
given(klient.pobierzImie()).willReturn("Katarzyna Gruszka");

//when
String wynik = klient.pobierzImieIDate();

//then
assertThat(wynik, equalTo("Katarzyna Gruszka 2022-01-11"));
}
}

Po utworzeniu obiektu klasy Klient oraz użyciu metody spy() na tej klasie, możemy bezpośrednio zwrócić się do poszczególnych metod tej klasy, aby wymusić ich określone zachowanie. Przy użyciu metody willReturn(), ustawiliśmy co bedą zwracały konktretne metody tej klasy, następnie poprzez wywołanie metody z klasy Klient (pobierzImieIDate()), możemy sprawdzić czy wartości zwrócone zgadzają się z danymi ustawionymi wcześniej. Widzimy, że wywołujemy w tym przypadku rzeczywiste metody klasy ktorą szpiegujemy.

Podsumowanie

Mam nadzieję, że przedstawiony powyżej przykład użycia dubli typu Stub, Mock oraz Spy, jest zrozumiały i podkreśla ich główne cechy charakterystyczne. Umiejętność pisania dubli testowych może okazać się bardzo przydatna. Pomimo, że z punktu widzenia technicznego, dubla testowego można napisać przy użyciu frameworka (choć nie każdego i nie zawsze), to czytelność i zarządzanie jest niekiedy lepsza na korzyść samodzielnie napisanej klasy. Nie zapominajmy jednocześnie, że możemy się również zetknąć z innymi typami dubli testowych.

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *