#17 — testy jednostkowe

By 7 lutego 2015Kurs Javy

Dzisiejsza lekc­ja jest pier­wszą doty­czą­ca nie tyle samej nau­ki pro­gramowa­nia, co doskonale­nia warsz­tatu pro­gramisty. Zajmiemy się automaty­czny­mi tes­ta­mi – tzw. Tes­ta­mi jed­nos­tkowy­mi.

W tej lekcji powiemy sobie o tym, czym są testy jed­nos­tkowe, jak je tworzyć oraz jak uży­wać ich ze Springiem. W przyszłoś­ci powiemy sobie także o tzw. mock­owa­niu i bib­liotekach wspo­ma­ga­ją­cych testowanie aplikacji.

Lekcja

Czym są testy jednostkowe

Testy jed­nos­tkowe to – moim zdaniem – najważniejszy z aspek­tów zapew­ni­a­nia jakoś­ci opro­gramowa­nia. Testy jed­nos­tkowe to nic innego jak zbiór testów (prób), które wery­fiku­ją, czy jed­nos­t­ka kodu (np. Klasa, ser­wis itp.) dzi­ała zgod­nie z oczeki­wa­ni­a­mi. Jeśli uży­wamy Mave­na, testy jed­nos­tkowe uruchami­ane są automaty­cznie przy każdym budowa­niu pro­jek­tu, jeśli kto­ryś z testów nie zadzi­ała, cała pro­ce­du­ra jest prz­ery­wana.

Testy jed­nos­tkowe są ważne z 2 powodów. Po pier­wsze przy tworze­niu opro­gramowa­nia pozwala­ją upewnić się, że nasz pro­gram dzi­ała poprawnie (a przy­na­jm­niej jego poszczególne częś­ci). Aby taka wery­fikac­ja była wiary­god­na, testy jed­nos­tkowe powin­ny testować nie tylko ‘oczeki­wany’ sce­nar­iusz (tzw. Hap­py case / hap­py path), ale także nieoczeki­wane ele­men­ty – np. błędne dane jako atry­bu­ty metod, brak danych itp. Dobrze napisany test to taki, kto­ry zaskoczył pro­gramistę pisza­cego kod, który tes­tu­je­my :)

Dru­gi powód to unikanie tzw. regresji – z dużym praw­dopodobieńst­wem kod, który pisze­my, będzie w przyszłoś­ci rozwi­jany i mody­fikowany. Może sie zdarzyć, że takie mody­fikac­je spowodu­ją, że coś, co wcześniej dzi­ałało praw­idłowo, przes­tanie dzi­ałać. Zami­ast czekać na reakc­je niezad­owolonych użytkown­ików, może­my takie prob­le­my wych­wycić już przy budowa­niu naszej aplikacji.

Testy jednostkowe w Maven

Maven jak wspom­ni­ałem wspiera automaty­czne testy jed­nos­tkowe. Aby je utworzyć, najpierw doda­je­my zależność do bib­liote­ki Junit http://mvnrepository.com/artifact/junit/junit, po czym może­my przys­tąpić do tworzenia testow.

Pami­etaj jed­nak, aby testy tworzyć w kat­a­logu src/test/java! Inaczej Maven ich nie będzie ‘widzi­ał’, więc nie zostaną one uru­chomione.

Przykładowe testy

Do testowa­nia może­my użyć kil­ka bib­liotek, z których najpop­u­larniejsza to Junit. Dal­szy opis będzie doty­czył wyko­rzys­ta­nia właśnie tej bib­liote­ki.

Nasz test skla­da się z zestawu poje­dynczych testów, z których każdy może testować inny przy­padek. Każdy z takich poje­dynczych przy­pad­ków to osob­na meto­da, która oznacza­my adno­tac­ja @Test . Wewnątrz tej metody real­izu­je­my logikę, po czym za pomocą metod staty­cznych klasy Assert tes­tu­je­my, czy otrzy­mane wyni­ki są zgodne z oczeki­wa­ni­a­mi:

public class KalkulatorTest {
    @Test
    public void mnozeniePrzezZeroZwracaZero() {
        Kalkulator kalkulator = new Kalkulator();
        Assert.assertEquals("0 * 0 musi sie rownac zero", 0, kalkulator.mnoz(0, 0));
    }
}

Powyższy test najpierw tworzy obiekt, który tes­tu­je­my, a następ­nie sprawdza, czy wynik metody dla konkret­nych argu­men­tów jest równy oczeki­wane­mu (w tym wypad­ku 0). Pier­wszy argu­ment to infor­ma­c­ja o błędzie — jeśli test się nie powiedzie, taki komu­nikat pojawi się w logach.
Jeśli chodzi o inne metody klasy Assert, zna­jdziemy je w doku­men­tacji JUnit. Z najpop­u­larniejszych moż­na wymienić:

  • assertTrue/assertFalse — sprawdza, czy wynik jest prawdą/fałszem
  • assert­Null — sprawdza, czy wynik jest nullem
  • assertE­quals — sprawdza, czy dwa ele­men­ty są równe (wywołu­je metody equals)
  • fail — powodu­je, że test jest prz­ery­wany i oznaczany jako niez­dany

Sprzątanie’ i przygotowanie do testów

JUnit pozwala nam także na wyko­nanie jakiegoś kodu przed i po poje­dynczym teś­cie jak i po całym zestaw­ie testów (czyli klasie, która posi­a­da metody z adno­tac­ja­mi @Test). Służą do tego adno­tac­je @Before, @BeforeClass, @After oraz @AfterClass . Przykład­owa klasa może wyglą­dać następu­ją­co:

public class KlasaTestowa {
    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        //ta metoda będzie wywołana raz, przed wszystkimi testami
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        //ta metoda będzie wywołana raz, po wszystkich testach
    }

    @Before
    public void setUp() throws Exception {
        //ta metoda będzie wywołana przed każdym testem
    }

    @After
    public void tearDown() throws Exception {
        //ta metoda będzie wywołana po każdym teście
    }

    @Test
    public void testujCos() {
        //...
    }
}

Zwróć uwagę, że metody, które są wywoły­wane tylko raz przed i po wszys­t­kich tes­tach są staty­czne! Metody te służą np. do przy­go­towa­nia danych do testów, ‘rese­towa­niu’ zmi­an wprowad­zonych przez poprzed­nie testy, ‘sprzą­ta­niu’ po tes­tach (np. zamyka­niu połączeń itp) — żad­na z nich nie jest obow­iązkowa, ale może­my je wyko­rzys­tać, jeśli potrze­bu­je­my.

Pokrycie kodu (code coverage)

Ter­min ten jest dość częs­to spo­tykany w kon­tekś­cie testów jed­nos­tkowych, w szczegól­noś­ci w dużych fir­ma­ch. Oznacza on ile pro­cent lin­i­jek kodu (nie licząc klamer, deklaracji itp.) jest real­nie wykony­wanych pod­czas wszys­t­kich testów. Inny­mi slowy, wyni­ki wyko­na­nia jakiej częś­ci kodu wery­fiku­je­my (w teorii). Zdarza­ją sie patolo­gie, jak np. wymóg 100% pokrycia (patrz niżej) – ogól­nie w sen­sown­ie napisanym sys­temie (tj. takim, w którym nie pisze­my zbed­nego kodu, uży­wamy adno­tacji, korzys­tamy z CoC itp.) dobrą wartoś­cią jest 70–85% pokrycia (w zależnoś­ci od tech­nologii, logi­ki biz­ne­sowej, złożonoś­ci itp.). Pokrycie poniżej 40% jest z kolei prze­ważnie bard­zo złym syg­nalem.

Co testujemy, czego nie testujemy

Tes­tu­je­my przede wszys­tkim logikę biz­ne­sową – czyli środ­kowa warst­wę (ser­wisy). Testy jed­nos­tkowe służą do tego, żeby upewnić się ze pewien pro­ces, algo­rytm został praw­idłowo przełożony na jezyk pro­gramowa­nia. Z tego powodu bard­zo częs­to nie tes­tu­je się w ten sposób kon­trol­erów (do tego prze­ważnie służą testy inte­gra­cyjne) – ponieważ one odpowiada­ją za ‘tłu­macze­nie’ wejś­cia od użytkown­i­ka na wewnętrzne struk­tu­ry sys­te­mu – jak i unikamy testowa­nia klas mod­elu (np. encji) – real­nie może­my przetestować get­tery i set­tery,  a więc w zasadzie tylko to, czy język Java nadal dzi­ała ;) Doda to kil­ka pro­cent do pokrycia, ale nie o to w tes­tach chodzi.

Testy jednostkowe i Spring

Spring wspiera tworze­nie testow jed­nos­tkowych, m.in. pozwala­jąc inicjować kon­tekst nie tylko w ramach aplikacji webowej. Poniżej przyk­lad testów, ktore korzys­ta­ją z kon­tek­stu Spring’a (zdefin­iowanego w pliku root-context.xml):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/root-context.xml")
public class SpringoweTesty {
    @Autowired
    private JakisSerwis serwisDoTestowania;

    @Test
    public void testujSerwis() {
        Assert.assertTrue(serwisDoTestowania.metodaKtoraPowinnaZwrocicTrue());
    }

    // inne testy ...
}

Dzię­ki takiej kon­fig­u­racji może­my korzys­tać np. z adno­tacji @Autowired, co zde­cy­dowanie upraszcza testowanie poszczegól­nych ele­men­tów :)

Podsumowanie

Dzisi­aj nauczyłaś się korzys­tać z pier­wszego z narzędzi służą­cych polep­sza­niu jakoś­ci kodu – wg mnie najważniejszego. Testy jed­nos­tkowe nie są tech­nicznie trud­nym zagad­nie­niem, ale trze­ba sporo wyobraźni, żeby przetestować wszys­tkie (tzn. jak najwięcej) sce­nar­iusze negaty­wne. Ważną rzeczą jest, zeby pamię­tać, czemu służą testy – to nie jest narzędzie do pod­wyzsza­nia code cov­er­age, to narzędzie do zapew­ni­a­nia jakoś­ci – jes­li Twoj sys­tem ma pokrycie 50%, ale cała logi­ka jest przetestowana – super, nie twórz na siłę bezsen­sownych testów :)

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzię­ki!

  •  
  •  
  •  
  •  
  •  
  • coding_monkey

    Witam. Należy sobie postaw­ić pytanie, czy jeżeli w teś­cie jed­nos­tkowym pod­pinamy kon­tekst springowy, to czy to jest jeszcze test jed­nos­tkowy, czy może już inte­gra­cyjny

    • To zależy od celu w jakim pod­pinamy ten kon­tekst. Jeśli tylko po to, by zainicjować kom­po­nent i jego zależnoś­ci (ewen­tu­al­nie pod­mienić cześć z nich) w celu przetestowa­nia go, to w takiej sytu­acji myślę, że nadal może­my mówić o tes­tach jed­nos­tkowych. Testy “inte­gra­cyjne” moż­na też napisać za pomocą unit testów nie pod­pina­jąc kon­tek­stu Springa, ale mówimy o zas­tosowa­niu zgod­nie z przez­nacze­niem.

      • coding_monkey

        Będę obstawał przy swoim, to znaczy:
        Jeżeli chcę jed­nos­tkowo przetestować klasę ser­wisu, to nie muszę wstrzyki­wać do inter­fe­j­su DAO jego konkret­nej imple­men­tacji, wystar­czy zamock­ować jego ist­nie­nie i metodą Mockito.verify(…) sprawdz­ić, czy ta meto­da się wykon­ała (nie ważne jak — prze­cież od tego jest test jed­nos­tkowy kole­jnej klasy). Po pros­tu. Właśnie w tes­tach jed­nos­tkowych “ogłu­pi­amy” klasy, to znaczy tes­tu­je­my tylko to, co same mają robić, a co robią pozostałe inter­fe­jsy ich już nie powin­no obchodz­ić.
        Z kolei test na zasadzie: wstrzyknę do ser­wisu konkret­ną imple­men­tację z kon­tenera DI i sprawdzę, czy rzeczy­wiś­cie ona pobrała/zapisała obiekt jest już innym testem (tu nie będę się upier­ać, czy inte­gra­cyjny, czy jeszcze inny), daleko wychodzą­cy poza odpowiedzial­ność klasy testowanej.

        • To co piszesz to oczy­wiś­cie praw­da, choć moim zdaniem trochę uproszcze­nie sytu­acji. Weźmy pod uwagę kil­ka innych czyn­ników:
          -> kon­tekst Springa daje nam obslugę adno­tacji typu @PostConstruct . Oczy­wiś­cie, może­my te metody wywoły­wać ręcznie, ale to z kolei wstrzyki­wanie logi­ki do testów
          -> wszys­tkie beany w kon­tekscie może­my pod­mienić, także na zamock­owane (widzi­ałem gdzieś imple­men­tac­je tagów dla mock­i­to, nie wiem jed­nak czy to jest nadal rozwi­jane, nie umiem też na szy­bko ich odnaleźć)
          -> w ten sposób moz­na tez sprawdz­ić, czy sam kon­tekst sie uruchamia (choć oczy­wi­scie robimy to jako osob­ny test)
          -> nie zawsze chce­my wszys­tko mock­owac — wezmy np. bezs­tanowe wal­ida­to­ry, helpery itp. Aby przetestowac logike, powin­ny one zwracac odpowiedz na pod­staw­ie wejs­cia z tes­tu wg swo­jej logi­ki (testowana klasa może mieć logikę, która opiera się o te sprawdzenia, mock­u­jąc je ryzyku­je­my niepoprawny test)
          -> nie zawsze mamy inne wyjś­cie — np. aby przetestować DAO real­i­zowane z uży­ciem Spring Data może­my albo zbu­dować kon­tekst, albo ręcznie zainicjować dao, mock­ować Data­Source itp (nie jestem pewny czy to możli­we, pewnie sprowadz­iło by się do ręcznej inic­jal­iza­cji kon­tek­stu Springa i tak). Oczy­wiś­cie nie mówię o testowa­niu, czy get­tery nadal dzi­ała­ją w Javie, ale o testowa­niu np. metod, w których uży­wamy włas­nego zapy­ta­nia. Dla mnie to nadal odpowiedzial­ność tes­tu jed­nos­tkowego (przy założe­niu, że nie korzys­tamy z zewnętrznej bazy danych tylko tworzymy jakąś w pamię­ci)

          Ogól­nie więc masz rac­je, że klase powin­niśmy testowac ‘w izo­lacji’. W wiek­szych pro­jek­tach nie zawsze jest to jed­nak mozli­we (chy­ba, ze w mock­ach dup­liku­je­my logikę innych klas, co z kolei tworzy kole­jne miejsce potenc­jal­nych błędów). W wielu sytu­ac­jach uzy­wanie kon­tek­stu Springa do testow po pros­tu upraszcza pracę, mimo, że nie jest ‘podreczn­nikowo’ zgodne ze sztu­ka.
          Jestem jed­nak zda­nia, ze to narzedzia maja sluzyc pro­gramis­tom a nie na odwrot, i dopo­ki lekko naduzy­wamy ich swiadomie, nie widze w tym nic zlego. Aczkol­wiek oczy­wi­scie lat­wo o sytu­acje, w ktorej zami­ast tes­tu jed­nos­tkowego robimy inte­gra­cyjny lub funkcjon­al­ny, ale jak ze wszys­tkim: “With great pow­er, comes great respon­si­bil­i­ty”.
          Nie twierdzę też, że to najlep­szy sposób na testowanie każdej klasy, ale jestem zda­nia, że warto znać tą metodę. Jeśli się nie przy­da, zmarnowal­iśmy jeden akapit tek­stu, jeśli się przy­da — zaoszczędzil­iśmy praw­dopodob­nie kil­ka godzin pra­cy i ręcznego pod­pina­nia komponentów/mocków w tes­tach.

  • Karol

    Dobrym pode­jś­ciem pisa­nia testów jest zasa­da giv­en-when-then. Warto by o niej wspom­nieć ;)

    • Jasne! Wydawało mi się, że gdzieś o tym wspom­i­namy, uzu­pełn­imy zatem i ten wpis ;)