wtorek, 16 października 2012

Logowanie interakcji w Mockito

Ostatnio przyszedł do mnie mój architekt i zaczął rozmowę coś w stylu:
Marcin, ty nasz ekspercie od testowania jednostkowego, integracyjnego i Mockito…
Ucieszony miłymi słowami pochwały i podbudowany faktem, że stanowię ważną część zespołu słuchałem dalej, co ma mi do powiedzenia kolega z zespołu. Wtedy jeszcze nie wiedziałem, że to pierwsze zdanie to tak naprawdę prosta, skuteczna, magiczna sztuczka, dzięki której bardzo łatwo jest później przekonać odbiorcę tego komunikatu, do wykonania pewnego zdania. To już trochę zahacza o manipulację / wywoływanie (ang. elicitation) / urabianie / programowanie neurolingwistyczne, czy też inne tego typu techniki, a więc wykracza po za tematykę tego bloga.

Wracając do rozmowy:
No bo właśnie mamy taki problem, że zrobiliśmy refaktoring niedawno stworzonej klasy i generalnie wszystko działa, ale trzeba jeszcze testy dopasować. Ten i tamten już poszli do domu, więc pytam czy byś tego nie zrobił. Uważam, że Tobie, jako że jesteś najbardziej w testach zorientowany, pójdzie szybko i gładko...
No dobra, myślę sobie. Dzisiaj kończy się czas przeznaczony na kodowanie w tym Sprincie, trzeba więc cos zdeployować, aby testerzy mieli jutro co robić. No dobra, obowiązki wzywają.

Architekt pokazał mi i opisał jakie to zmiany zostały wykonane. Generalnie był to pewien proces, który kodowaliśmy przez cały sprint. Miał on wiele danych wejściowych i generował sporo na wyjściu. No i wcześniej była sobie taka jedna metoda, która to miała 6 argumentów, kilka mocków i produkowała coś pożytecznego. Zostało to opakowane w fabrykę, która podawała teraz cześć danych od dupy strony (wywołanie innych serwisów), jak i również sama metoda została przeorana. Czyli liczba argumentów zmalała, doszło kilka zależności, nowy sposób instancjowania, no i trzeba był do tego przerobić testy.

Cofnięcie zmian nie wchodziło w rachubę, gdyż była to sprawka w sumie 3ch osób, a wykorzystywany system kontroli wersji (dziękujemy IBM Synergy) zrobił by więcej złego niż dobrego. Olanie testów jedynie przesuwało problem w czasie, a na syndrom wybitego okna nie mogłem sobie pozwolić.

Zacząłem więc to po kolei przerabiać testy, ich czytelność malała, a definicje zachowania mocków stawały się niezrozumiałe. Najgorsze że pierwszy test, który przerabiałem, ciągle nie działał. Nie wiedziałem w końcu czy to wina domyślnych wartości zwracanych przez Mocki z Mockito, czy też trefnego refactoringu (a raczej redesignu).

No i w tym momencie postanowiłem sprawdzić interakcje z moimi mockami, czy czasem gdzieś przypadkiem Mockito nie zwraca np. pustej listy, która to powoduje brak wyników. Chwila szperania w dokumentacji i znalazłem taki ciekawy twór. Przykładowy kod poniżej.
@Test
public void shouldCreateAccount() {
    // given
    InvocationListener logger = new VerboseMockInvocationLogger();
    AccountService accountService = new AccountService();
    StrengthPasswordValidator validator = mock(
            StrengthPasswordValidator.class,
            withSettings().invocationListeners(logger));
    UserDao userDao = mock(UserDao.class,
            withSettings().invocationListeners(logger));
    UserFactory userFactory = mock(UserFactory.class,
            withSettings().invocationListeners(logger));

    when(userFactory.createUser(LOGIN, PASSWORD))
            .thenReturn(new User(LOGIN, PASSWORD));

    accountService.setPasswordValidator(validator);
    accountService.setUserDao(userDao);
    accountService.setUserFactory(userFactory);

    // when
    User user = accountService.createAccount(LOGIN, PASSWORD);

    // then
    assertEquals(LOGIN, user.getLogin());
    assertEquals(PASSWORD, user.getPassword());
} 
Testujemy zakładanie konta użytkownika. Życiowy przykład, wiele nie trzeba tłumaczyć.

Na początek w linii 4tej tworzymy instancję obiektu typu VerboseMockInvocationLogger. Jest to standardowa implementacja interfejsu InvocationListener, która wypisuje na standardowe wyjście zaistniałe interakcje z mockami. Aby przekazać ten obiekt do naszych mocków, musimy podczas ich tworzenia, ustawić ten obiekt. Jak? Widzimy to w linii 8smej. Powtarzamy to dla każdego mocka, który nas interesuje.

Dalej standardowo. Za pomocą when() konfigurujemy Mock’a, injectujemy zaleznosci (linie 17-19) wywołanie metody testowej i assercje. Dzięki takiemu Testu możemy otrzymać interesujący output:

########### Logging method invocation #1 on mock/spy ########
userFactory.createUser("login", "password");
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.mockinvocations.AccountServiceTest.shouldCreateAccount(AccountServiceTest.java:31)
   has returned: "null"

############ Logging method invocation #2 on mock/spy ########
strengthPasswordValidator.validate(
    "password"
);
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.AccountService.createAccount(AccountService.java:19)
   has returned: "0" (java.lang.Integer)

############ Logging method invocation #3 on mock/spy ########
userDao.findUserByLogin("login");
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.AccountService.existAccount(AccountService.java:43)
   has returned: "null"

############ Logging method invocation #4 on mock/spy ########
   stubbed: -> at com.blogspot.mstachniuk.unittestpatterns.service.mockinvocations.AccountServiceTest.shouldCreateAccount(AccountServiceTest.java:31)
userFactory.createUser("login", "password"); 
   invoked: -> at com.blogspot.mstachniuk.unittestpatterns.service.AccountService.createAccount(AccountService.java:23)
   has returned: "User{login='login', password='password'}" (com.blogspot.mstachniuk.unittestpatterns.domain.User)
Pierwsze wywołanie, to te w teście:
when(userFactory.createUser(LOGIN, PASSWORD))
        .thenReturn(new User(LOGIN, PASSWORD));
Niestety (a może stety) nie można ukryć tego wywołania w standardowym outpucie, chyba że się napisze własna implementację InvocationListener’a.

W wywołaniu #2 widzimy, że wywołano metodę validate() z parametrem "password" i zwrócono zero. Co prawda metoda ta zwraca void, ale to pierdoła i nie należy się nią przejmować. Co fajniejsze, to po kliknięciu na AccountService.java:19 zostaniemy przeniesieni w miejsce wywołania tej metody. Bardzo użyteczny feature.

Trzecia interakcja jest analogiczna, ale za to czwarta jest ciekawa. Widzimy z jakimi parametrami wywołano mockowaną metodę, z którego miejsca w kodzie i gdzie zostało to zachowanie zdefiniowane! Od razu można przejrzeć konfigurację testu i się zorientować co skąd się bierze. Jak komuś za mało / za dużo, to zawsze można samemu sobie zaimplementować InvocationListener'a. Dzięki temu można trochę lepiej poznać bebechy tej biblioteki.

To tyle odnośnie tej logowania interakcji w Mockito. Obyście musieli jej używać jak najrzadziej, gdyż jak dla mnie świadczy to o skomplikowaniu i zagubieniu we własnych kodzie. Warto jednak o czymś takim zawczasu wiedzieć, może czasem uratować tyłek. Mi dzięki temu udało się dojść co było nie tak.

Cały kod wykorzystywany w tym wpisie można ściągnąć z githuba, a prezentowany kod testowy znajduje się w klasie: AccountServiceTestWithMockInvocations.

Morał tej historii:
Zwracaj uwagę na to co i jak mówią inni i nie daj się zmanipulować! Zwłaszcza w pracy :)