Testowanie w dobie pandemii (stub,mock i spy)

Tło

Tłem dzisiejszego wpisu jest ostatnio popularny temat jakim jest pandemia choroby COVID-19. Na początek ,żeby nie marnować nikomu czasu, nie będzie to wpis o testach na koronawirusa a o testach jednostkowych.

Dla omówienia tematu testów jednostkowych posłużę się tematem raportowania danych zakażeń którymi zajmują się wojwódzkie oddziały sanepidu. Pozwolę sobie na fantazjowanie, jakby to było ciekawie gdyby można było stworzyć taki system, gdzie można by rzetelnie raportować o przypadkach zachorowań w poszczególnych powiatach. Próbowałem zgłębić ten temat, poszukująć źródeł ale niestety każdy wojewódzki inspektoriat sanitarny rządzi się swoimi prawami. Pokusiłem się o stworzenie własnych map i na szybko powstała strona covid-map.pl wykorzystująca kompozycję biblioteki leaflet.js, geoservera oraz skryptów pythona które za pomocą web-scrapingu dane takie pobierają z zawartości stron internetowych.

Tutaj skupię się na javie. Stworzymy prototyp aplikacji, która będzie zbierała dane dotyczące przypadków COVID-19 z wojewódzkich stacji sanitarno-epidemiologicznych, na przykładzie którego omówię koncepty testowania jednostkowego.

Program

Zanim przejdę to testów, przedstawię uproszczony prototyp aplikacji którą wykorzystamy to przedstawienia testów. Zaczynamy od klasy która będzie zawierać pojedynczą obserwację, nazwiemy ją „Report”:

public class Report {
    String powiat;
    int infections;
    int deaths;
    int recoveries;
//a tutaj konstruktor oraz settery i gettery
 }

Następnie tworzymy interfejs Który nazwiemy „CovidData” w którym to znajduje się metoda „fetchReports()” i taki interfejs ma zostać zaimplementowany dla klas reprezentujących stacje sanepidu. Może każda stacja ma swoją bazę danych (SQL,noSQL) a może mają tabelki w excelu? A oto interfejs:

public interface CovidData {
    public List<Report> fetchReports();
}

A na koniec tworzymy klasę która będzie wykorzystywać klasy implementujące powyższy interfejs oraz obliczała użyteczne statystyki. Klasę tą nazwiemy „CovidStats” i ta klasa na razie posiada metodę zliczającą wszystkie przypadki zakażeń:

public class CovidStats {

private CovidData covidData;

public CovidStats(CovidData covidData) {
this.covidData = covidData;
}

public int AggregateInfections(){
int sum = covidData.fetchReports().stream().mapToInt(Report::getInfections).sum();
System.out.println("Liczba nowych zakazen: "+sum);
return sum;
}
}

Metoda AggregateInfections będzie zwracała sumę nowych zakażeń i tą metodę poddamy testom.

Stub

A oto co oznacza słowo stub:

the short part of something that is left after the main part has been used, especially a cigarette after it has been smoked or one of the small pieces of paper left in a book from which cheques or tickets have been torn

https://dictionary.cambridge.org/pl/dictionary/english/stub

Stub to inaczej niedopałek papierosa. Biorąc do ust taki niedopałek mamy jedynie namiastkę tego co daje nam cały papieros (bardzo niedydaktyczny przykład, zachęcający do palenia ale to wina tego kto wymyślił te całe stuby). I właśnie tak też jest w przypadku stosowania stubów w testowaniu.

Stworzony został interfejs ale brakuje implementacji która być może łączy się z bazą danych do której nie mamy dostępu więc dla naszego testowania musimy sobie taką implementację zasymulować za pomocą klasy „stubowej”. Klasa ta powstaje oczywiście w folderze testowym:

public class CovidDataStub implements CovidData {
    private List<Report> reportList;
    @Override
    public List<Report> fetchReports() {

        Report Krakow = new Report("Krakow",40,2,30);
        Report Tarnow = new Report("Tarnow",20,1,12);
        Report Nowy_Sacz = new Report("Nowy Sacz",10,1,3);
        
        reportList = Arrays.asList(Krakow,Tarnow,Nowy_Sacz);
        return reportList;
    }
}

W klasie tej zaimplementowaliśmy metodę „fetchReports” w której na sztywno stworzyliśmy raport zachorowań dla kliku wybranych powiatów. A teraz możemy wykorzystać tą klasę do stworzenia metody testowej:

class CovidStatsStubTest {

    @Test
    void checkIfAggregateInfectionsReturnsValidInteger() {
        //given
        CovidDataStub covidDataStub =new  CovidDataStub();
        CovidStats stats = new CovidStats(covidDataStub);

        //when
        int sum=stats.AggregateInfections();

        //then
        assertThat(sum,greaterThanOrEqualTo(0));
    }
}

Do konstruktora klasy CovidStats przekazujemy klasę stubową potem tworzymy prostą asercję sprawdzającą czy metoda sumująca liczbę zachorowań jest większa lub równa „0”

Mock

Mocking to kpina albo drwina ale najtrafniejszym określeniem tutaj byłoby zdecydowanie przedrzeźnianie. Przedrzeźniacz imituje klasę bądź interfejs i może przedrzeźniać w dowolny sposób, a także w domyślny. Spójrzmy na metodę testową wykorzystującą mocka:

void checkIfAggregateInfectionsReturnsValidInteger() {
        //given
        CovidData covidData = mock(CovidData.class);
        CovidStats stats = new CovidStats(covidData);
        //when
        int sum=stats.AggregateInfections();
        //then
        assertThat(sum,greaterThanOrEqualTo(0));
    }

Na przykładzie powyższym wykorzystujemy metodę mock której jako argument podajemy klasę przedrzeźnianą. Najprostsze zastosowanie domyślnego mocka to tzw „nice mock” który zwróci domyślne wartości zmiennych zwracanych. To znaczy jeśli wywołamy metodę klasy przedrzeźnianej, a tutaj zwracającej liczbę zakażonych to wartość domyślna będzie wynosić zero dla zmiennych typu int. Jeśli chcemy przetestować inny scenariusz to musimy stworzyć metodę pomocniczą która zwraca obiekt który nasza mockowana metoda używa.

private List<Report> generateReportData(){
        Report Krakow = new Report("Krakow",40,2,30);
        Report Tarnow = new Report("Tarnow",20,1,12);
        Report Nowy_Sacz = new Report("Nowy Sacz",10,1,3);
        Report limanowski = new Report("limanowski",4,0,0);

        return Arrays.asList(Krakow,Tarnow,Nowy_Sacz,limanowski);
    }

A teraz, żeby skorzystać z tego, musimy zmienić metodę testową:

void checkIfAggregateInfectionsReturnsValidInteger() {
    //given
    List<Report> report_test_data = generateReportData();
    CovidData covidData = mock(CovidData.class);
    CovidStats stats = new CovidStats(covidData);
    given(covidData.fetchReports()).willReturn(report_test_data);

    //when
    int sum=stats.AggregateInfections();

    //then
    assertThat(sum,greaterThanOrEqualTo(0));
}

Dodano obiekty typu List<Report> któremu przypisujemy obiekt zwracany przez metodę pomocniczą. Następnie zmuszamy obiekt przedrzeźniający czli covidData żeby jego metoda zwróciła obiekt który chcemy wykorzystać w teście.

Spy

Spy czyli szpieg. Metoda hybrydowa, która umożliwia tworzenie obiektów typu spy które wykorzystują metody mockowe a zarazem możemy wywoływać prawdziwe metody. Żeby to zademonstrować możemy zrobić prosty test. W tym celu wykorzystamy implementacje interfejsu CovidData czyli naszego Stuba (po polsku można go nazwać kiepem) i dodamy do niego metodę:

public int getNumberOfReports(){
        try{
            return this.fetchReports().size();
        }
        catch (NullPointerException e){
            System.out.println(e.getMessage());
            return 0;
        }
    }

Ta metoda zwraca nam liczbę obserwacji/raportów w liście.

Następnie przetestujemy tę metodę:

void testIfNumberOfObesrvationsIsCorrect() {
    //given
    CovidDataStub covidData = spy(CovidDataStub.class);
    given(covidData.fetchReports()).willReturn(generateReportData());

    //when
    int obs= covidData.getNumberOfReports();
    
   //then
   assertThat(obs,equalTo(4));
}

W sekcji given następuje instancjacja klasy CovidDataStup przy użyciu metody spy.

Na tej klasie dokonujemy wymuszenia zwrócenia listy raportów poprzez użycie metody pomocniczej dla naszej klasy testowej która była przedstawiona powyżej. Należy zwrócić uwagę że tam były 4 powiaty a nie 3 jak to było oryginalnie w klasie Stubowej. Metoda willReturn wymusza zwrócenie właśnie tej nowej listy.

W sekcji when wywołujemy prawdziwą metodę która zlicza ilość powiatów w klasie. Tutaj należy podkreślić różnice pomiędzy mockiem szpiegiem a prawdziwą klasą. Gdybyśmy w seksji given zamiast spy użyli mock to w tym momencie metoda getNumberOfReports zwróciła by wartość 0 ,powodując błąd testu. Natomiast jeśli zamiast metody spy wykorzystalibyśmy prawdziwą klasę to oczywiście nie moglibyśmy wymusić zwracania żądanych wartości jak w metodzie willReturn.

W sekcji then dokonujemy asercji sprawdzając czy lista ma 4 elementy. Test przechodzi!

Podsumowanie

Te bardzo proste przykłady pokazują różnice pomiędzy metodami testowania. Powyższe przykłady zostały opracowane w Intellij przy użyciu maven. Należy pamiętać o odpowiednim skonfigurowaniu projektu i dodaniu odpowiednich dependencji w pliku pom.

Testowanie w dobie pandemii (stub,mock i spy)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *