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