Testy jednostkowe – Mocki

Mocki to obiekty, które imitują zachowanie prawdziwych obiektów i prawdziwego kodu. Zadaniem programisty jest zaprogramowanie odpowiedniego działania mocka.

Ten wpis jest drugą częścią miniserii o stubach oraz mockach. Poznamy w nim zalety mocków, a także ich ogólną charakterystykę i zastosowanie. Pod tym adresem znajdziesz część pierwszą, w której omawiane są stuby.

Jak mocki, to Mockito

Aby w ogóle móc skorzystać z obiektów mockowych, należy dodać do projektu zależność w postaci frameworka Mockito. Najlepiej ściągnąć najnowszą wersję, aktualnie jest to wersja 2.25.

My polecamy skorzystanie z zależności, która jest przystosowana do współpracy z frameworkiem JUnitmockito-junit-jupiter.

Odpowiedni artefakt znajdziemy korzystając na przykład z wyszukiwarki na stronie maven.org lub też bezpośrednio z tego odnośnika. Pod tym adresem znajdziemy gotowy plik *.jar lub też gotowy wpis, który możemy wkleić do pliku konfiguracyjnego Mavena lub Gradla.

Czym są mocki?

Mocki to obiekty, których zadaniem jest symulacja działania normalnych obiektów i kodu. Natomiast zadaniem programisty jest odpowiednie zaprogramowanie działania mocka.

Na przykład: jeżeli metoda zostanie wywołana obiekcie mockowym X, to ma zwrócić wartość Y.

Mocki mogą być tworzone dynamicznie w czasie runtime’u aplikacji i są znacznie bardziej elastyczne w porównaniu do stubów. Zapewniają też znacznie więcej funkcjonalności.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Jak je tworzyć?

Aby porównać mocki bezpośrednio ze stubami, utworzymy klasę testową AccountServiceTest. Jej zadanie będzie takie samo, jak klasy testowej AccountServiceStubTest, gdzie testowaliśmy metodę z serwisu AccountService. (we wpisie z zeszłego tygodnia dotyczącym stubów)

Możemy więc skopiować cały kod z klasy AccountServiceStubTest. Nasza klasa testowa będzie zatem wyglądała tak:

@Test
void getAllActiveAccounts() {

    //given
    AccountRepository accountRepositoryStub = new AccountRepositoryStub();
    AccountService accountService = new AccountService(accountRepositoryStub);

    //when
    List<Account> accountList = accountService.getAllActiveAccounts();

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

}

Aby teraz zamienic stuba na obiekt mockowy, możemy skorzystać ze specjalnej metody frameworka Mockitomock. Ta metoda pozwala nam na utworzenie obiektu mockowego z danej klasy. W naszym przykładzie będzie to oczywiście klasa AccountRepository:

AccountRepository accountRepositoryMock = mock(AccountRepository.class);

I takiego mocka możemy teraz przekazać do konstruktora klasy AccountService:

AccountRepository accountRepositoryMock = mock(AccountRepository.class);
AccountService accountService = new AccountService(accountRepositoryMock);

Zatem jak było widać na powyższym przykładzie, w bardzo prosty sposób jesteśmy w stanie zamienić obiekt stubowy na obiekt mockowy.

Jednak jeśli teraz uruchomimy tę metodę testową, to asercja nie zostanie spełniona, a test nie przejdzie. W wyniku zobaczymy, że była oczekiwana wartość 2, a w rzeczywistości zwrócona została wartość 0, a więc pusta lista. Dlaczego?

Miłe mocki

Dzieje się tak dlatego, że w Mockito wykorzystywane są tak zwane „nice mocki„. Działają one w specyficzny sposób. Załóżmy, że mamy taką sytuację: w klasie, którą mockujemy znajdują się metody, które zwracają jakieś wartości. Wtedy domyślnie Mockito przy wywołaniu takich metod postara się zwrócić jakieś sensowne wartości, a nie na przykład po prostu  wartość null.

W naszym przykładzie, gdzie zwracana jest lista obiektów typu Account, Mockito zwraca pustą listę. A jeśli metoda miałaby zwracać wartości Integer, to Mockito zwróciłoby wartość 0. Jeśli metoda zwracałaby wartość boolean, to Mockito zwróciłoby wartość false. Więcej informacji można znaleźć w dokumentacji Mockito

W taki sposób będą zachowywały się mocki, jeśli nie zaprogramujemy dla nich żadnego działania. Ale właśnie… przecież możemy to zachowanie zaprogramować!

Szybko, łatwo i przyjemnie

Najpierw musimy przygotować listę danych testowych. Na szczęście mamy już taki zestaw w naszej klasie stubowej, znanej z poprzedniego wpisu. A żeby nie zaśmiecać naszej metody testowej, to dodatkowo ten kod umieścimy w metodzie pomocniczej:

private List<Account> prepareAccountData() {
    Address address1 = new Address("Kwiatowa", "33/5");
    Account account1 = new Account(address1);

    Account account2 = new Account();

    Address address2 = new Address("Piekarska", "12b");
    Account account3 = new Account(address2);

    return Arrays.asList(account1, account2, account3);
}

Dla przypomnienia, klasa Account ma postać:

class Account {

    private boolean active;
    private Address defaultDeliveryAddress;

    Account() {
        this.active = false;
    }

    Account(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
        if(defaultDeliveryAddress != null) {
            activate();
        } else {
            this.active = false;
        }
    }

    void activate() {
        this.active = true;
    }

    boolean isActive() {
        return this.active;
    }

    Address getDefaultDeliveryAddress() {
        return defaultDeliveryAddress;
    }

    void setDefaultDeliveryAddress(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
    }

}

Jeśli przy tworzeniu obiektu typu Account skorzystamy z konstruktora bezargumentowego lub też w konstruktorze oczekującym obiektu typu Address przekażemy wartość null, to wtedy utworzone konto będzie nieaktywne, a więc metoda isActive będzie zwracała wartość false. W przeciwnym razie oczywiście zwrócona będzie wartość true.

W naszym przykładzie utworzone zostały 3 konta: account1, account2 i account3. account2 zostało utworzone korzystając z konstruktora bezargumentowego, więc to konto będzie nieaktywne. account1 i accoun3 natomiast będą aktywne.

Możemy teraz wrócić do naszej metody testowej: getAllActiveAccounts . Tutaj chcielibyśmy móc zaprogramować takie działanie:

Jeżeli na mocku zostanie wywołana metoda getAllAccounts , to w wyniku powinna być zwrócona lista kont z metody prepareAccountData.

Aby to zrobić, korzystamy ze specjalnych metod pomocniczych z biblioteki Mockito: when oraz thenReturn:

when(accountRepository.getAllAccounts()).thenReturn(accounts);

Taka składnia jest popularna, ale może być dla kogoś myląca, bo mamy tu kombinację słów when i then, dość podobne do samych sekcji //given, //when i //then.

Zatem żeby zachować ducha Behaviour Driven Development (BDD), można skorzystać z innej kombinacji metod: given i willReturn.

Po zmianach cała metoda testowa prezentuje się następująco:

@Test
void getAllActiveAccounts() {

    //given
    List<Account> accounts = prepareAccountData();
    AccountRepository accountRepository = mock(AccountRepository.class);
    AccountService accountService = new AccountService(accountRepository);
    given(accountRepository.getAllAccounts()).willReturn(accounts);

    //when
    List<Account> accountList = accountService.getAllActiveAccounts();

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

}

I teraz gdy uruchomimy testy, czeka nas miły widok:

stuby mocki testy jednostkowe junit mockito dev foundry blog programowanie java spring kursy

Elastyczność

Powyższy przykład już jasno pokazuje, że mocki są znacznym ułatwieniem względem stubów. Chociażby dlatego, że nie musimy tworzyć osobnej klasy stubowej. Ale jeszcze większą zaletą jest to, jak łatwo możemy tworzyć kolejne scenariusze testowe.

Załóżmy, że chcielibyśmy przetestować sytuację, w której z bazy danych nie zwrócone zostaną żadne konta.

Zatem do klasy testowej dopisujemy kolejną metodę. Nazwiemy ją getNoActiveAccounts.

Część główną metody możemy dla ułatwienia skopiować z metody getAllActiveAccounts. Dodatkowo musimy tutaj dokonać zmiany logicznej. Oczekujemy, że lista kont, którą otrzymamy z bazy danych, będzie pusta. Zmieniamy zatem asercję z:

assertThat(accountList, hasSize(2));

na:

assertThat(accountList, hasSize(0));

I modyfikujemy zawartość metody thenReturn tak, aby obiekt mockowy faktycznie zwracał nam pustą listę (można tu skorzystać na przykład z utilsowej metody Javowej):

given(accountRepository.getAllAccounts()).willReturn(Collections.emptyList());

Ostatecznie cała metoda testowa wygląda tak:

@Test
void getNoActiveAccounts() {

    //given
    AccountRepository accountRepository = mock(AccountRepository.class);
    AccountService accountService = new AccountService(accountRepository);
    given(accountRepository.getAllAccounts()).willReturn(Collections.emptyList());

    //when
    List<Account> accountList = accountService.getAllActiveAccounts();

    //then
    assertThat(accountList, hasSize(0));

}

Możemy teraz ją uruchomić. Asercja powinna być spełniona i ponownie powinniśmy ujrzeć zielony kolor. To takie proste!

A jeśli w interfejsie pojawią się nowe metody, to również w niczym to nie przeszkodzi. Wtedy po prostu utworzy się stosownego mocka i zaprogramuje odpowiednie działanie.

Podsumowanie

Powyższe przykłady jasno pokazują dlaczego mocki są takie popularne i dlaczego zazwyczaj są lepszym rozwiązaniem od stubów. Dają programistom bardzo duże możliwości i zapewniają elastyczność w działaniu. Dzięki nim można też przetestować znacznie więcej scenariuszy testowych, oszczędzając przy tym dużo czasu.


Podziel się tym wpisem:

1 komentarz do wpisu “Testy jednostkowe – Mocki”

Dodaj komentarz