Pokazywanie postów oznaczonych etykietą catch-exception. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą catch-exception. Pokaż wszystkie posty

piątek, 30 marca 2012

Nowa ksiazka o testowaniu jednostkowym


Dnia 28 marca 2012 pojawiła sie oficjalnie książka: Practical Unit Testing with TestNG and Mockito napisana przez Tomka Kaczanowskiego. Wspominałem w jednym z poprzednich wpisów (na temat testowania wyjatków), że ta książka nadchodzi. Już oficjalnie jest dostępna do kupienia na practicalunittesting.com do czego zachecam.

Dlaczego o tym pisze?

Powodów jest kilka.

Po pierwsze jest to książka polskiego autora (co prawda po angielsku, ale spolszczenie jest w przygotowaniu), a tych należy wspierać. Łatwiej (podobno) jest napisać książkę techniczną po angielsku i później ją przetłumaczyć na nasze, niż pisać po polsku i później się męczyć.

Po drugie jest to kompletny, uporządkowany i aktualny zbiór technik z przykładami, jak pisać dobre testy. Książka wyjaśnia wszelkie wątpliwości związane z nazewnictwem, konwencjami i dobrymi praktykami, jak i daje wiele cennych wskazówek.

Dla kogo jest ta książka?

Książka jest bardzo dobra dla osób, które chcą właśnie zacząć pisać testy jednostkowe do swojego kodu i nie wiedzą jak wystartować. Książka wprowadza krok po korku do lepszego świata kodu pokrytego testami. Również dla startych wyjadaczy znajdzie się sporo cennych informacji. Dalsze rozdziały książki omawiają zaawansowane zagadnienia ze świata testowania. Tomek wskazuje również dobre praktyki jak pisać testy, a także prezentuje narzędzia powiązane z nimi(jak catch-exception czy testy mutacyjne).

Dlaczego więc się wypowiadam o tej książce, skoro dopiero co się ukazała?

Dobra, dość tej krypto reklamy, czas na konkrety ;)

Powodem dla którego piszę na blogu o tej książce, jest fakt, że brałem udział w jej powstawaniu. Mianowicie robiłem korektę (review) książki, za co dostałem podziękowania uwiecznione na jednej ze stron w książce:
In various different ways a number of people have helped me with writing this book – some by giving feedback, others by comforting me in times of doubt.
Marcin Stachniuk was the first person to offer to review the book in the early stages of its being written, and at the same time the most persistent of my reviewers. He read every part of the book and gallantly overcame the obstacles I frequently put in his way: frequent releases, constant juggling of the contents, minor piecemeal adjustments, etc.
Nadszedł teraz czas podzielenia się moimi wrażeniami ze wspólnej pracy z autorem.

Wszystko zaczęło się na konferencji GeeCON 2011 w Krakowie, Podczas (za)ostatniego dnia konferencji (tzw. Community Day) Tomek miał swoją prezentację pt.: Who watches the watchmen? - on quality of tests. Na prezentacji trochę się wynudziłem, gdyż otarłem się już o większość omawianych tam zagadnień. Pod koniec jednak prelegent pochwalił się, że właśnie pisze książkę o testach. Wówczas jeden z uczestników (nazwisko do wiadomości redakcji) zaproponował, że chętnie by zrobił review jego książki. Bardzo zaciekawiła mnie ta inicjatywa, i mimo iż wszyscy już wychodzili z sali, zostałem chwilę dłużej, aby przysłuchać się rozmowie na temat recenzowania książki.

Tomek zaproponował aby chętni się do niego zgłosili mail’owo, aby ustalić szczegóły współpracy.  I tak to się zaczęło. Stwierdziliśmy, że najlepszą formą wymiany informacji będzie założona na Google prywatna grupa dyskusyjna, poprzez którą Tomek będzie podsyłać nowe wersje książki. Tym kanałem można również podyskutować i wyjaśnić ewentualne kwestie sporne. Rozwiązanie to sprawdziło się.

Preferowanym sposobem zgłaszania uwag, było odsyłanie aktualnego pdf’a ze wstawionymi komentarzami w teksie, w formie żółtych karteczek. Funkcjonalność taką daje nam Foxit Reader. Dzięki temu Tomek widział dokładnie, którego miejsca dotyczy uwaga. Było to również wygodne dla mnie, gdyż fragmenty czytałem na ekranie komputera. Niektórzy zgłaszali swoje uwagi w postaci wiadomości na grupie, ale dla mnie żółte karteczki i tak były lepsze, gdyż dokładniej wskazywały miejsce, gdzie jest coś nie tak.

Na podstawie e-maili naliczyłem jakieś 20 „release’ów” książki, tzn. fragmentów, które Tomek podsyłał do korekty. Początkowo przychodziły pojedyncze rozdziały do czytania, ale lepszym wyjściem, jak się później okazało, było podsyłanie całości. Dzięki temu nie było rozdziałów wyrwanych z kontekstu i było widać ogólnego zarys całości książki, w którą stronę ona dąży.

Koleje wersje pojawiały się z różną częstotliwością. Czasem było kilka w tygodniu, a czasem była cisza przez dłuższy czas. Ale to już była kwestia Tomka jak sobie organizuje pracę nad książką. Ze swojej strony mogę powiedzieć, że często nowe wersje się pojawiały, jak akurat mocno byłem zajęty innymi sprawami i niemiałem możliwości aby usiąść od razu do lektury. Kilka razy dogoniła mnie kolejna wersja i wtedy już trzeba było się zmobilizować i przeczytać to co tam Tomek naskrobał :)

Co do zgłaszanych uwag, to Tomek zazwyczaj je akceptował. Jak nie widziałem poprawy w następnej wersji, to początkowo się czepiałem, że moje sugestie są nieuwzględniane, ale w późniejszym czasie doszedłem do wniosku, że nie powinienem się tym przejmować. W końcu to książka Tomka i Jego koncepcja, a nie moja. Jedynie sprawy edytorsko/estetyczne (zła czcionka, rozjeżdżające się tabelki) były dla mnie bardzo irytujące, ale to chyba przez spędzenie dawniej sporej ilości czasu z LaTeX’em. Tomek postanowił zostawić to sobie na sam koniec, przez co ciągle widziałem te niedoskonałości. To była chyba jedyna pierdółka, która mi troszkę przeszkadzała w naszej współpracy.

Moje zaangażowanie zostało wynagrodzone cytowanymi wyżej podziękowaniami w książce (Dzięki Tomek!). Właściwie to nie nawet nie ustaliliśmy szczegółów współpracy, tzn. co ewentualnie będę z tego miał. Do samego końca nie wiedziałem nawet, czy pojawią się te podziękowania w książce (po cichu na nie liczyłem). W końcu w jednej z ostatnich wersji książki zobaczyłem, że są :) Świadczy to o tym, że moje zaangażowanie było kompletnie non-profit.

Podsumowując współpracę cieszę się, że wziąłem udział w tym przedsięwzięciu. Było to bardzo ciekawe doświadczenie i mogłem przez to poznać proces powstawania książki. Robienie review sprawia wielka frajdę, czego zwieńczeniem jest oczywiście ukazanie się książki „na produkcji”. Szczerze polecam każdemu zaangażowanie się w tego typu akcję.


Btw. Co do relacji z 33 degree (kilka osób już o nią pytało), to musi ona jeszcze poczekać kilka dni, z powodu natłoku innych zajęć (w tym korekty ostatecznej wersji książki Tomka), jak i obecnych sporych zmian życiowych.

sobota, 4 lutego 2012

Testowanie wyjątków

Testowanie wyjątków w testach jednostkowych od zawsze trochę mnie irytowało. Niby sprawa banalna, ale jak do tego dołączymy słynny szablon // given // when // then to nie bardzo wiadomo gdzie powinniśmy umieścić słówko // then, aby test był dalej przejrzysty.

Problem nie jest nowy. Zastanawiano się nad dobrym rozwiązaniem już podczas prezentacji Bartosza Bańkowskiego i Szczepana Fabera pt. Pokochaj swoje testy [czas - 18.04] na Wroc JUG, albo pewnie jeszcze wcześniej. Idąc za radą lepszych, zawsze korzystałem z try / catch’a, aby wiedzieć gdzie dokładnie spodziewam się wyjątku.

@Test
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    try {
        // when
        someClass.doSomething();
        fail("This method should throw SomeException");
    } catch(SomeException e) {
        // then
        assertThat(e.getMessage()).isEqualTo("Some message");
    }

}


Blok instrukcji try / catch wymusza na nas pewien sposób formatowania kodu i przez to nie do końca widać gdzie jest // when i // then. Można oczywiście próbować umieszczać je w trochę innym miejscu, np. // when przed try, a // then przed fail().

@Test
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    try {
        someClass.doSomething();
        // then
        fail("This method should throw SomeException");
    } catch(SomeException e) {
        assertThat(e.getMessage()).isEqualTo("Some message");
    }

}

Jednak rozwiązanie dalej jest nie do końca czytelne. Gdyby jeszcze nic nie umieszczać w bloku catch, to już w ogóle, trzeba się chwilę zastanowić, co my tu tak na prawdę chcemy przetestować. Całe szczęście narzędzia do statycznej analizy kodu dbają o to, aby nie zostawiać pustych bloków catch.

Alternatywnym rozwiązaniem dla testowania rzucanych wyjątków, jest stosowanie andotacji @Test z parametrem expected w JUnit’cie  :

@Test(expected = SomeException.class)
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    someClass.doSomething();

    // then
    fail("This method should throw SomeException");
}

Test wygląda już lepiej, ale ma swoje wady. Jak mi jakiś test nie przechodzi, to pierwsze co robię, to czytam sekcję // then testu. W tym przypadku widzę wywołanie fail() co sugeruje mi, że test zawsze powinien nie przechodzić. Dopiero po chwili zauważam, że test został zadeklarowany jako @Test(expected = SomeException.class), czyli spodziewam się wyjątku typu SomeException. Jest tutaj jednak pewne niebezpieczeństwo. Jeżeli faza // given testu, czyli przygotowania środowiska testowego, będzie trochę dłuższa, to może się zdarzyć, że tam gdzieś poleci wyjątek. Test będzie dalej przechodził, a tak naprawdę nie będzie testowane to co chcieliśmy. Wspominał o tym już Szczepan Faber w cytowanym fragmencie video. Dodatkowo nie można, jeśli byśmy chcieli sprawdzić np. treść komunikatu wyjątku. Z tych względów nie stosowałem tej konstrukcji.

Sprawa wygląda trochę  lepiej w przypadku TestNG. Tutaj mamy analogiczna adnotację, mianowicie zamiast expected używamy expectedExceptions.

@Test(expectedExceptions = SomeException.class,
        expectedExceptionsMessageRegExp = "Some Message.*")
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    someClass.doSomething();

    // then
    fail("This method should throw SomeException");
}

Tu jeszcze dochodzi fajna zabawka w postaci expectedExceptionsMessageRegExp, czyli możemy za pomocą wyrażeń regularnych sprawdzić, czy wyjątek posiada spodziewaną wiadomość. Dalej jednak istnieje ryzyko wyrzucenia tego wyjątku w sekcji // given.

Podobną zabawkę daje nam JUnit, ale w trochę innym wydaniu. Mianowicie od wersji 4.7 można zastosować ExpectedException:

public class SomeClassTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void shouldThrowSomeException() throws Exception {
        // given
        thrown.expect(SomeException.class);
        thrown.expectMessage("Some Message");
        SomeClass someClass = new SomeClass();

        // when
        someClass.doSomething();

        // then
        fail("This method should throw SomeException");
    }
}

Tutaj w klasie testowej musimy zdefiniować regułę (linie 3 i 4), która początkowo mówi, że nie spodziewamy się wyjątków. Natomiast już w naszych metodach testowych, redefiniujemy to zachowanie i mówimy, czego się spodziewamy w danym teście (linie 9 i 10). Możemy dzięki temu sprawdzić komunikat rzucanego wyjątku. Tutaj jednak podajemy fragment wiadomości, którą ma zawierać nasz wyjątek. Istnieje również przeciążona wersja tej metody, która jako argument przyjmuje Matcher’a Hamcrest’owego.

Do niedawna były to jedyne rozwiązania, jakie były dostępne w temacie testowania wyjątku. Jakiś czas temu jednak pojawiła się ciekawa biblioteka: catch-exception. Kod napisany za jej pomocą może wyglądać tak:

@Test
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    caughtException(someClass).doSomething();

    // then
    assertThat(caughtException())
            .isInstanceOf(SomeException.class)
            .as("Some Message");

}


Czyli mamy metodę CatchException.catchException(), gdzie jako argument przekazujemy obiekt naszej klasy. Następnie wywołujemy metodę, którą chcemy przetestować. Na koniec w sekcji // then sprawdzamy czy otrzymaliśmy wyjątek, którego się spodziewaliśmy. Bezargumentowa wersja caughtException() zwraca wyjątek rzucony przez ostatnią klasę, którą przekazaliśmy do metody caughtException(). W naszym wypadku jest to ostatni wyjątek wygenerowany przez someClass.

I to rozwiązanie mi się podoba. W sekcji // when informuję, że będę łapał wyjątki, a w sekcji // then sprawdzam czy poleciał ten wyjątek, którego oczekiwałem. I do tego nie bruździ to przy formatowaniu kodu i użyciu // given // when // then. I mamy czytelny kod :)

Zafascynowany tą biblioteką postanowiłem zajrzeć do środka (kod jest dostępny na googlecode.com), aby zobaczyć jak zbudowano takie rozwiązanie.

public class CatchException {

    public static <T> T catchException(T obj) {
        return processException(obj, Exception.class, false);
    }

}

Czyli mamy delegację pracy do metody processException(), która zwraca obiekt tego samego typu, jaki został przekazany w argumencie. Dzięki temu możemy używać biblioteki w sposób jaki pokazałem powyżej. Zobaczmy co kryje się za tą metodą:

private static <T, E extends Exception> T processException(T obj,
        Class<E> exceptionClazz, boolean assertException) {

    if (obj == null) {
        throw new IllegalArgumentException("obj must not be null");
    }

    return new SubclassProxyFactory().<T> createProxy(obj.getClass(),
            new ExceptionProcessingInterceptor<E>(obj, exceptionClazz,
                    assertException));

}

Po upewnieniu się, że argument nie jest null’em tworzymy (jak można się domyśleć po nazwach) proxy dla naszej klasy. Brnąc dalej w las, jeżeli klasa nie jest ani prymitywem, ani finalna, to proxy tworzone jest  w ten sposób:

proxy = (T) ClassImposterizer.INSTANCE.imposterise(
        interceptor, targetClass);

czyli wykorzystywany jest ExceptionProcessingInterceptor z poprzedniegu listingu, wewnątrz którego znajduje się następująca metoda, gdzie już widać całą magię:

public Object intercept(Object obj, Method method, Object[] args,
        MethodProxy proxy) throws Throwable {

    beforeInvocation();

    try {
        Object retval = proxy.invoke(target, args);
        return afterInvocation(retval);
    } catch (Exception e) {
        return afterInvocationThrowsException(e, method);
    }

}

Metoda beforeInvocation() czyści ostatnio złapany wyjątek, np. z wywołania poprzedniej metody. Następnie w bloku try wywoływana jest nasza rzeczywista metoda (linia 5), a następie zwracana jest (w naszym sposobie wykorzystania biblioteki) wartość wygenerowana przez oryginalną metodę. Jak coś pójdzie nie tak, to w bloku catch jest zapamiętywany rzucony wyjątek (zajmuje się tym metoda afterInvocationThrowsException()). Bardzo sprytny sposób na łapanie wyjątków, a jaki banalny zarazem.

Z ciekawostek jeszcze, to biblioteka korzysta z Mockito, a dokładniej cglib. Nie działa ona z klasami finalnymi (gdyż dla nich nie można teoretycznie tworzyć proxy), no chyba że skorzystamy w PowerMock’a i odpowiednich adnotacji w deklaracji klasy testowej:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ SomeClass.class })
public class SomeClassFinalPowerTest {
    // ...
}


Wtedy zadziała :)



Na koniec wpisu, jeszcze informacja skąd się dowiedziałem o tej bibliotece. Mianowicie powstaje teraz ciekawa książka o testach: Practical Unit Testing with TestNG and Mockito. Pisana jest ona przez Tomka Kaczanowskiego i już niedługo ujrzy światło dzienne. Książka poszła już do recenzji do Szczepana Fabra, więc lipy nie będzie. Będzie można się z niej dowiedzieć m.in. o catch-exception, jak i o testach mutacyjnych, o których pisałem ostatnio. Na razie wyjdzie wersja angielska, ale będzie też robione tłumaczenie na nasz ojczysty język. Zachęcam więc do śledzenia informacji o książce jak i do jej zakupu.

Więcej informacji o tym jak powstawała książka w kolejnych wpisach.