#17 – testy jednostkowe

By 7 lutego 2015Kurs Javy
Wpis-Header lekcje

Dzisiejsza lekcja jest pierwszą dotycząca nie tyle samej nauki programowania, co doskonalenia warsztatu programisty. Zajmiemy się automatycznymi testami – tzw. Testami jednostkowymi.

W tej lekcji powiemy sobie o tym, czym są testy jednostkowe, jak je tworzyć oraz jak używać ich ze Springiem. W przyszłości powiemy sobie także o tzw. mockowaniu i bibliotekach wspomagających testowanie aplikacji.

Lekcja

Czym są testy jednostkowe

Testy jednostkowe to – moim zdaniem – najważniejszy z aspektów zapewniania jakości oprogramowania. Testy jednostkowe to nic innego jak zbiór testów (prób), które weryfikują, czy jednostka kodu (np. Klasa, serwis itp.) działa zgodnie z oczekiwaniami. Jeśli używamy Mavena, testy jednostkowe uruchamiane są automatycznie przy każdym budowaniu projektu, jeśli ktoryś z testów nie zadziała, cała procedura jest przerywana.

Testy jednostkowe są ważne z 2 powodów. Po pierwsze przy tworzeniu oprogramowania pozwalają upewnić się, że nasz program działa poprawnie (a przynajmniej jego poszczególne części). Aby taka weryfikacja była wiarygodna, testy jednostkowe powinny testować nie tylko ‘oczekiwany’ scenariusz (tzw. Happy case / happy path), ale także nieoczekiwane elementy – np. błędne dane jako atrybuty metod, brak danych itp. Dobrze napisany test to taki, ktory zaskoczył programistę piszacego kod, który testujemy :)

Drugi powód to unikanie tzw. regresji – z dużym prawdopodobieństwem kod, który piszemy, będzie w przyszłości rozwijany i modyfikowany. Może sie zdarzyć, że takie modyfikacje spowodują, że coś, co wcześniej działało prawidłowo, przestanie działać. Zamiast czekać na reakcje niezadowolonych użytkowników, możemy takie problemy wychwycić już przy budowaniu naszej aplikacji.

Testy jednostkowe w Maven

Maven jak wspomniałem wspiera automatyczne testy jednostkowe. Aby je utworzyć, najpierw dodajemy zależność do biblioteki Junit http://mvnrepository.com/artifact/junit/junit, po czym możemy przystąpić do tworzenia testow.

Pamietaj jednak, aby testy tworzyć w katalogu src/test/java! Inaczej Maven ich nie będzie ‘widział’, więc nie zostaną one uruchomione.

Przykładowe testy

Do testowania możemy użyć kilka bibliotek, z których najpopularniejsza to Junit. Dalszy opis będzie dotyczył wykorzystania właśnie tej biblioteki.

Nasz test sklada się z zestawu pojedynczych testów, z których każdy może testować inny przypadek. Każdy z takich pojedynczych przypadków to osobna metoda, która oznaczamy adnotacja @Test . Wewnątrz tej metody realizujemy logikę, po czym za pomocą metod statycznych klasy Assert testujemy, czy otrzymane wyniki są zgodne z oczekiwaniami:

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 testujemy, a następnie sprawdza, czy wynik metody dla konkretnych argumentów jest równy oczekiwanemu (w tym wypadku 0). Pierwszy argument to informacja o błędzie – jeśli test się nie powiedzie, taki komunikat pojawi się w logach.
Jeśli chodzi o inne metody klasy Assert, znajdziemy je w dokumentacji JUnit. Z najpopularniejszych można wymienić:

  • assertTrue/assertFalse – sprawdza, czy wynik jest prawdą/fałszem
  • assertNull – sprawdza, czy wynik jest nullem
  • assertEquals – sprawdza, czy dwa elementy są równe (wywołuje metody equals)
  • fail – powoduje, że test jest przerywany i oznaczany jako niezdany

‚Sprzątanie’ i przygotowanie do testów

JUnit pozwala nam także na wykonanie jakiegoś kodu przed i po pojedynczym teście jak i po całym zestawie testów (czyli klasie, która posiada metody z adnotacjami @Test). Służą do tego adnotacje @Before, @BeforeClass, @After oraz @AfterClass . Przykładowa klasa może wyglądać następują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ływane tylko raz przed i po wszystkich testach są statyczne! Metody te służą np. do przygotowania danych do testów, ‚resetowaniu’ zmian wprowadzonych przez poprzednie testy, ‚sprzątaniu’ po testach (np. zamykaniu połączeń itp) – żadna z nich nie jest obowiązkowa, ale możemy je wykorzystać, jeśli potrzebujemy.

Pokrycie kodu (code coverage)

Termin ten jest dość często spotykany w kontekście testów jednostkowych, w szczególności w dużych firmach. Oznacza on ile procent linijek kodu (nie licząc klamer, deklaracji itp.) jest realnie wykonywanych podczas wszystkich testów. Innymi slowy, wyniki wykonania jakiej części kodu weryfikujemy (w teorii). Zdarzają sie patologie, jak np. wymóg 100% pokrycia (patrz niżej) – ogólnie w sensownie napisanym systemie (tj. takim, w którym nie piszemy zbednego kodu, używamy adnotacji, korzystamy z CoC itp.) dobrą wartością jest 70-85% pokrycia (w zależności od technologii, logiki biznesowej, złożoności itp.). Pokrycie poniżej 40% jest z kolei przeważnie bardzo złym sygnalem.

Co testujemy, czego nie testujemy

Testujemy przede wszystkim logikę biznesową – czyli środkowa warstwę (serwisy). Testy jednostkowe służą do tego, żeby upewnić się ze pewien proces, algorytm został prawidłowo przełożony na jezyk programowania. Z tego powodu bardzo często nie testuje się w ten sposób kontrolerów (do tego przeważnie służą testy integracyjne) – ponieważ one odpowiadają za ‘tłumaczenie’ wejścia od użytkownika na wewnętrzne struktury systemu – jak i unikamy testowania klas modelu (np. encji) – realnie możemy przetestować gettery i settery,  a więc w zasadzie tylko to, czy język Java nadal działa ;) Doda to kilka procent do pokrycia, ale nie o to w testach chodzi.

Testy jednostkowe i Spring

Spring wspiera tworzenie testow jednostkowych, m.in. pozwalając inicjować kontekst nie tylko w ramach aplikacji webowej. Poniżej przyklad testów, ktore korzystają z kontekstu Spring’a (zdefiniowanego 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 konfiguracji możemy korzystać np. z adnotacji @Autowired, co zdecydowanie upraszcza testowanie poszczególnych elementów :)

Podsumowanie

Dzisiaj nauczyłaś się korzystać z pierwszego z narzędzi służących polepszaniu jakości kodu – wg mnie najważniejszego. Testy jednostkowe nie są technicznie trudnym zagadnieniem, ale trzeba sporo wyobraźni, żeby przetestować wszystkie (tzn. jak najwięcej) scenariusze negatywne. Ważną rzeczą jest, zeby pamiętać, czemu służą testy – to nie jest narzędzie do podwyzszania code coverage, to narzędzie do zapewniania jakości – jesli Twoj system ma pokrycie 50%, ale cała logika jest przetestowana – super, nie twórz na siłę bezsensownych testów :)

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • coding_monkey

    Witam. Należy sobie postawić pytanie, czy jeżeli w teście jednostkowym podpinamy kontekst springowy, to czy to jest jeszcze test jednostkowy, czy może już integracyjny

    • To zależy od celu w jakim podpinamy ten kontekst. Jeśli tylko po to, by zainicjować komponent i jego zależności (ewentualnie podmienić cześć z nich) w celu przetestowania go, to w takiej sytuacji myślę, że nadal możemy mówić o testach jednostkowych. Testy „integracyjne” można też napisać za pomocą unit testów nie podpinając kontekstu Springa, ale mówimy o zastosowaniu zgodnie z przeznaczeniem.

      • coding_monkey

        Będę obstawał przy swoim, to znaczy:
        Jeżeli chcę jednostkowo przetestować klasę serwisu, to nie muszę wstrzykiwać do interfejsu DAO jego konkretnej implementacji, wystarczy zamockować jego istnienie i metodą Mockito.verify(…) sprawdzić, czy ta metoda się wykonała (nie ważne jak – przecież od tego jest test jednostkowy kolejnej klasy). Po prostu. Właśnie w testach jednostkowych „ogłupiamy” klasy, to znaczy testujemy tylko to, co same mają robić, a co robią pozostałe interfejsy ich już nie powinno obchodzić.
        Z kolei test na zasadzie: wstrzyknę do serwisu konkretną implementację z kontenera DI i sprawdzę, czy rzeczywiście ona pobrała/zapisała obiekt jest już innym testem (tu nie będę się upierać, czy integracyjny, czy jeszcze inny), daleko wychodzący poza odpowiedzialność klasy testowanej.

        • To co piszesz to oczywiście prawda, choć moim zdaniem trochę uproszczenie sytuacji. Weźmy pod uwagę kilka innych czynników:
          -> kontekst Springa daje nam obslugę adnotacji typu @PostConstruct . Oczywiście, możemy te metody wywoływać ręcznie, ale to z kolei wstrzykiwanie logiki do testów
          -> wszystkie beany w kontekscie możemy podmienić, także na zamockowane (widziałem gdzieś implementacje tagów dla mockito, nie wiem jednak czy to jest nadal rozwijane, nie umiem też na szybko ich odnaleźć)
          -> w ten sposób mozna tez sprawdzić, czy sam kontekst sie uruchamia (choć oczywiscie robimy to jako osobny test)
          -> nie zawsze chcemy wszystko mockowac – wezmy np. bezstanowe walidatory, helpery itp. Aby przetestowac logike, powinny one zwracac odpowiedz na podstawie wejscia z testu wg swojej logiki (testowana klasa może mieć logikę, która opiera się o te sprawdzenia, mockując je ryzykujemy niepoprawny test)
          -> nie zawsze mamy inne wyjście – np. aby przetestować DAO realizowane z użyciem Spring Data możemy albo zbudować kontekst, albo ręcznie zainicjować dao, mockować DataSource itp (nie jestem pewny czy to możliwe, pewnie sprowadziło by się do ręcznej inicjalizacji kontekstu Springa i tak). Oczywiście nie mówię o testowaniu, czy gettery nadal działają w Javie, ale o testowaniu np. metod, w których używamy własnego zapytania. Dla mnie to nadal odpowiedzialność testu jednostkowego (przy założeniu, że nie korzystamy z zewnętrznej bazy danych tylko tworzymy jakąś w pamięci)

          Ogólnie więc masz racje, że klase powinniśmy testowac ‚w izolacji’. W wiekszych projektach nie zawsze jest to jednak mozliwe (chyba, ze w mockach duplikujemy logikę innych klas, co z kolei tworzy kolejne miejsce potencjalnych błędów). W wielu sytuacjach uzywanie kontekstu Springa do testow po prostu upraszcza pracę, mimo, że nie jest ‚podrecznnikowo’ zgodne ze sztuka.
          Jestem jednak zdania, ze to narzedzia maja sluzyc programistom a nie na odwrot, i dopoki lekko naduzywamy ich swiadomie, nie widze w tym nic zlego. Aczkolwiek oczywiscie latwo o sytuacje, w ktorej zamiast testu jednostkowego robimy integracyjny lub funkcjonalny, ale jak ze wszystkim: „With great power, comes great responsibility”.
          Nie twierdzę też, że to najlepszy sposób na testowanie każdej klasy, ale jestem zdania, że warto znać tą metodę. Jeśli się nie przyda, zmarnowaliśmy jeden akapit tekstu, jeśli się przyda – zaoszczędziliśmy prawdopodobnie kilka godzin pracy i ręcznego podpinania komponentów/mocków w testach.

  • Karol

    Dobrym podejściem pisania testów jest zasada given-when-then. Warto by o niej wspomnieć ;)

    • Jasne! Wydawało mi się, że gdzieś o tym wspominamy, uzupełnimy zatem i ten wpis ;)