Pokazywanie postów oznaczonych etykietą Mockito. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Mockito. Pokaż wszystkie posty

poniedziałek, 1 września 2014

Implementacja Singleton’a w Javie

Ostatnio, podczas z jednej z rozmów rekrutacyjnych, dostałem pytanie, "A jak by Pan zaimplementował singleton w Javie?". Pytanie dość standardowe, mógł by je zadać każdy. Odpowiadam więc: „Klasa, z prywatnym konstruktorem, z polem statycznym o typie zadeklarowanej klasy, metoda getInstnce(), itd.”. Rekruter na to: "No dobra, a jakiś inny pomysł?".

Na szybko nie przychodziło mi jednak nic do głowy... Wtedy padło pytanie, które zmotywowało mnie do przygotowania tego wpisu: "A czytał Pan Effective Java, Joshua Bloch?". A no nie czytałem i nie sądzę, abym ją przeczytał.

Dlaczego?

Przede wszystkim uważam że ta książka jest trochę przestarzałą. Drugie wydanie pochodzi z 2008 roku i informacje w niej zawarte są trochę nieaktualne. To nie książka o wzorcach projektowych Bandy Czworga, która nie traci na swojej aktualności, a jedynie pozycja o tym jak coś tam dobrze zrobić w Javie. I tak przykładowo rozdział: "Item 51 Beware the performance of string concatenation", traktujące o tym, aby lepiej używać StringBuilder’a niż + do sklejania tekstów, jest już dawno nieaktualne! Chciałem kiedyś pisać posta o tym, ale na stackoverflow i jeszcze gdzieś tam już dawno o tym było. Nie wiem tylko od kiedy dokładnie istnieje ten mechanizm zamiany + na StringBuilder’a w Javie.

O innych dobrych praktykach, można się dowiedzieć zawsze z innych źródeł, niekoniecznie czytając wspomnianą książkę.

Dobra wróćmy do tematu wpisu. W innym rozdziale Effective Java (Item 3: Enforce the singleton property with a private constructor or an enum type), jest zalecenie, aby Singletona implementować za pomocą Enuma. Z tym rozwiązaniem spotkałem się po raz pierwszy podczas review kodu kogoś, kto czytał tę książkę. Użycie wówczas enuma w roli singletonu było dla mnie zupełnie niezrozumiałe! Musiałem dopytać autora o co chodzi.

Dlaczego nie lubię tego rozwiązania? Spójrzmy na przykładowy kawałek kodu (inspirowany Joshuą Blochem, co bym za dużo nie musiał wymyślać, jak tu mądrze użyć singletona). Kod jest i tak kiepski (obliczanie czasu), ale chodzi mi o zaprezentowanie działania omawianego wzorca.
public enum Elvis {
    INSTANCE;

    private final int ELVIS_BIRTHDAY_YEAR = 1935;

    public int howOldIsElvisNow() {
        return new GregorianCalendar().get(Calendar.YEAR) - ELVIS_BIRTHDAY_YEAR;
    }
}

public class ElvisProfitService {

    public double ELVIS_SALARY_YEAR = 70_000;

    public double calculateElvisProfit() {
        return Elvis.INSTANCE.howOldIsElvisNow() * ELVIS_SALARY_YEAR;
    }
}

No i weź tu panie przetestuj taki kod! Możemy jeszcze Elvisa statycznie zaimportować, to linijka 6 skróci się do jeszcze mniej czytelnej formy.

return INSTANCE.howOldIsElvisNow() * ELVIS_SALARY_YEAR;

Ktoś ma jakieś pomysły jak taki kod przetestować? Da się oczywiście, z wykorzystaniem PowerMock’a [link do rozwiązania na końcu ^1], ale chyba nie o to chodzi aby pisać nietestowany kod?

Dlaczego wolę starszą wersję tegoż rozwiązania:

public class Elvis {

    private static final Elvis INSTANCE = new Elvis();

    private final int ELVIS_BIRTHDAY_YEAR = 1935;

    private Elvis() {
    }

    public static Elvis getInstance() {
        return INSTANCE;
    }

    public int howOldIsElvisNow() {
        return new GregorianCalendar().get(Calendar.YEAR) - ELVIS_BIRTHDAY_YEAR;
    }
}

Dochodzi prywatny konstruktor, statyczna metoda getInstance() i inicjalizacja pola klasy wraz z deklaracją.

W tym przypadku kod korzystający z tego singletonu, mógłby być następujący:

public class ElvisProfitService {

    private final double ELVIS_SALARY_YEAR = 70_000;
    private Elvis elvis = Elvis.getInstance();

    public double calculateElvisProfit() {
        return elvis.howOldIsElvisNow() * ELVIS_SALARY_YEAR;
    }

    // For tests
    void setElvis(Elvis elvis) {
        this.elvis = elvis;
    }
}

W linii 4 wywołałem getInstance(), aby w przykładowym kodzie produkcyjnym było wszystko cacy. Dzięki temu, zależność ta jest definiowana jako pole w kasie i mamy setter do tego, więc możemy sobie bardzo ładnie przetestować tą funkcjonalność, bez hackowania z PowerMockiem:

public class ElvisProfitServiceTest {

    @Test
    public void shouldCalculateElvisProfit() {
        // given
        ElvisProfitService service = new ElvisProfitService();
        Elvis elvis = mock(Elvis.class);
        when(elvis.howOldIsElvisNow()).thenReturn(1);
        service.setElvis(elvis);

        // when
        double elvisProfit = service.calculateElvisProfit();

        // then
        assertEquals(70_000, elvisProfit, 0.1);
    }
}

W sekcji given mamy bardzo ładnie zdefiniowane zachowanie, za pomocą Mockito, jakie ma przyjmować masz singleton na potrzeby tego testu.

A Ty jak definiujesz (o ile to robisz) swoje singletony? Które rozwiązanie uważasz za lepsze?



[1] Co do rozwiązania zagadki, jak przetestować Singletona jako Enum’a, to tutaj jest odpowiednia rewizja na github’ie: SingletonInJava e714fb a kod poniżej:

@RunWith(PowerMockRunner.class)
@MockPolicy(ElvisMockPolicy.class)
public class ElvisProfitServiceTest {

    @Test
    public void shouldCalculateElvisProfit() {
        // given
        ElvisProfitService service = new ElvisProfitService();

        // when
        double elvisProfit = service.calculateElvisProfit();

        // then
        assertEquals(70_000, elvisProfit, 0.1);
    }
}

public class ElvisMockPolicy implements PowerMockPolicy {

    @Override
    public void applyClassLoadingPolicy(MockPolicyClassLoadingSettings settings) {
        settings.addFullyQualifiedNamesOfClassesToLoadByMockClassloader("com.blogspot.mstachniuk.singletoninjava.Elvis");
    }

    @Override
    public void applyInterceptionPolicy(MockPolicyInterceptionSettings settings) {
        Method method = Whitebox.getMethod(Elvis.class, "howOldIsElvisNow");
        settings.proxyMethod(method, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return 1.0;
            }
        });
    }
}

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 :)

czwartek, 19 kwietnia 2012

Migracja z EasyMock’a na Mockito

Dostałem wczoraj w projekcie zadanie zmigrowania testów korzystających z EasyMock’a na Mockito. Zadanie to przypadło mi, ponieważ jakiś czas temu udało mi się wprowadzić do projektu Mockito, co uważam za duży sukces, gdyż jak wiadomo inicjowanie zmian w projektach nie jest łatwe.

Poniżej przedstawiam jak mi poszło.

9:00 Zbudowałem projekt, odpaliłem testy i zapisałem ile ich jest.
9:02 Sprawdziłem, że mam tylko 3 testy wykorzystujące EasyMock’a. W tym momencie stwierdziłem, że nie ma co szukać w Google jakiegoś migratora, ściągać, instalować i zastanawiać się, jak go uruchomić. Przy tak nie wielkiej liczbie klas do zmiany szkoda zachodu i lepiej zrobić to prymitywną metodą Search & Replace.
9:16 Skończyłem migrować 1wszą klasę.
9:19 Po przyjrzeniu się klasie zrobiłem jeszcze refaktoring, bo jak nie teraz to kiedy? Później nigdy nie nadejdzie.
9:20 Zacząłem modyfikować następną klasę.
9:27 Zakończyłem migrację i refaktoring. Wziąłem się za ostatnią klasę.
9:34 Ukończyłem modyfikacje dla 3ciej klasy.
9:35 Uruchomiłem wszystkie testy – działa.
9:41 Usunąłem wpisy dotyczące EasyMock’a w pom’ach Mavenowych. Tutaj przydała się funkcjonalność „Szukaj w plikach”, jaką daje Notepad++, abym mógł wyszukać wszystkie wpisy.
9:42 Odpaliłem builda i testy w Eclipse - zakończone sukcesem.
9:43 Odpaliłem builda i testy za pomocą konsoli – również zakończone sukcesem.
9:44 Rozpocząłem proces synchronizacji z repozytorium.
9:46 Zmiany zacomitowane.
9:52 Wystartował build na serwerze CI.
10:00 Build zakończył się jednym fail’ujacym testem. Dziwne, bo lokalnie działało. Jako że nie mam dostępu do konfiguracji Jenkinsa to i tak bym nie zidentyfikował problemu. Podszedłem do problemu pragmatycznie: usunąłem nadmiarowe wywołanie verify() i nieużywanego Mocka w jednym teście.
10:08 Rozpocząłem synchronizację z repozytorium.
10:11 Zmiany zacomitowane.
10:12 Wystartował kolejny build.
10:23 Build zakończony sukcesem, banan na ustach, można iść na herbatę.

Na całe szczęście mój obecny projekt jest jeszcze we wczesnej fazie wytwarzania, więc nie było dużo do roboty. Przy czymś większym, gdzie EasyMock jest namiętnie używany, łatwiej było by napisać jakiś skrypcik robiący brudną robotę za nas. Dodatkowo mój zespół ma niewielką wiedzę odnośnie używania mock’ów (i pisania dobrych testów), więc mam kogo ewangelizować :)

Poniżej jeszcze prezentuję przekształcenia z jakich korzystałem. Nie jest to oczywiście pełen zestaw, ale może on stanowić zajawkę Waszych migracji, albo może pomóc zrozumieć różnice między tymi bibliotekami.

import static org.easymock.EasyMock.createMock;
import static org.mockito.Mockito.mock;
createMock
mock
import static org.easymock.EasyMock.expect;
import static org.mockito.Mockito.when;
expect
when
andReturn
thenReturn
import static org.easymock.EasyMock.verify;
import static org.mockito.Mockito.verify;
Elementy do usunięcia:
import static org.easymock.EasyMock.replay;
replay

Do rozpatrzenia:
Przy wywołaniach verify() trzeba dopisać metodę z argumentami jaka powinna się wywołać. Albo jeszcze lepiej zastanowić się nad sensownością testu, gdyż często verify() jest zbędne. Ponadto anyTimes() zazwyczaj trzeba usunąć.

Na więcej problemów nie natrafiłem, ale zdaję sobie sprawę z tego, że przy większym systemie, gdzie EasyMock jest namiętnie wykorzystywany, może nie być tak łatwo. Przy okazji migracji można od razu wyłapać testy które są niedobrze napisane lub są niepoprawne z punktu widzenia Mockito, np. wywołanie verify() przed testowaną metodą.