Menu Zamknij

Jak pisać Testy Jednostkowe w Javie i Nie Zwariować?

Testy jednostkowe w java

Testy jednostkowe to jedna z tych rzeczy, których początkujący programiści nie cierpią. Zazwyczaj wtedy też ich nie piszą. Jak ich już tak nie cierpią i nie piszą, to szybko dopadają ich problemy. Zmiany w ich projektach coraz częściej, pokrywają się z wprowadzeniem nowych błędów, a poprawa tych błędów wiąże się z wprowadzeniem innych błędów. Zamiast rozwijać produkt, zaczynają się kręcić w niekończącej spirali bug-fixingu, systematycznie dostarczając użytkownikom nowe pakiety błędów.

Pisanie testów może stać się jednym z ulubionych obowiązków programisty. Wystarczy tylko wiedzieć, o co w tym wszystkim chodzi i jak to robić.

Twój kod zazwyczaj nie działa

Nie bierz na wiarę tego, że coś działa, tylko dlatego, że ty to napisałeś. Jeżeli uważasz, że coś jest tak proste, że nie idzie tego zepsuć. To często okazuje się, że jednak można. Prawie zawsze wkradnie się jakiś większy lub mniejszy błąd.

Nie jesteś nieomylny. Testuj!

Nie możesz w programowaniu kierować się intuicją i ślepą wiarą, że wszystko będzie dobrze. Zazwyczaj będzie niedobrze. Warto więc ubezpieczyć się od tych wpadek pisząc testy. Uwierz mi, że możesz na testy przeznaczyć 70% czasu, a to i się zwróci.

Błąd nie powinien cię boleć. To nienapisanie testu powinno boleć.

Błędy, to błędy, normalna sprawa. Błędy wkradają się wszędzie, postaram się wskazać ci najbardziej narażone miejsca, które szczególnie warto testować.

Co warto testować?

Warunki

Warto w swoim kodzie testować wszystkie warunki. Jako warunki rozumiem tutaj zachowania wydzielonego kodu, które są warunkowane danymi wejściowymi. Zazwyczaj okazuje się, że są to te miejsca, w których najłatwiej zrobić błąd. Do tego często bywa on tragiczny skutkach.

Wyrzucanie wyjątków.

Kolejna sprawa to wyrzucanie wyjątków. Niby mała istotna sprawa, ale pamiętaj, że jeżeli coś działa, gdy nie powinno, to skutki mogą być jeszcze gorsze niż położenie aplikacji. Tak więc testuj czy twój kod sprawdza się w wyjątkowych sytuacjach, bo te pojawią się szybciej, niż myślisz.

Konwersje, mapowania i transport danych.

Często dane są po prostu przepisywane z obiektu do obiektu. Wydawałoby się, że to coś, gdzie nie można się pomylić. To prawda ciężko to zepsuć, ale gdy jest tego dużo, łatwo o czymś zapomnieć. Więc napisz prosty test, by sprawdzić, czy czegoś po drodze nie zgubiłeś.

Modyfikacje danych

Modyfikacje danych to jedne z ważniejszych operacji w systemie. Aby aplikacja działała dobrze, dane muszą być spójne z jakąś rzeczywistością. Błędy w ich modyfikacji mogą skutkować odłączeniem się systemu o rzeczywistości. Idea używania takiego systemu traci wtedy trochę sens.

Rozmiar skrzynki ma Znaczenie!

Są większe i mniejsze jednostki testowe. Nie zawsze musi to być pojedyncza klasa. Może to być również moduł, pakiet, warstwa, pojedyncza funkcja lub jeden konkretny przypadek biznesowy.

Ważne by taka jednostka była dla ciebie czarną skrzynką. Im ta czarna skrzynka jest większa i czarniejsza, tym więcej możesz w niej zmieniać, nie psując swoich testów. To kwestia perspektywy, z jaką patrzysz na to, co wymaga przetestowania.

Większa perspektywa pozwala, tworzyć testy bardziej odporne na zmiany w kodzie. Niestety, jeżeli przesadzimy z rozmiarem naszej skrzynki, to takie testy jednostkowe mogą być mało czytelne i testować zbyt wiele rzeczy. Warto więc zachować umiar co do jej rozmiaru.

Liczy się tylko Wejście i Wyjście

Testuj tylko to czy dane wyjściowe pasuje do tych wejściowych.

Sprawdzaj tylko to, czy dla pewnych danych wejściowych masz oczekiwane dane wyjściowe.

Wiadomo, że żeby dobrze przygotować skrzynkę do testów, trzeba jakoś zaślepić zależności, które nie wchodzą w jej skład. Zaślepki jednak prawie zawsze okazują się kolejnymi punktami wejścia i wyjścia dla naszej czarnej skrzynki. Trzeba więc sprawdzać, co tędy wchodzi, a co wychodzi.

Return i argumenty metody to nie jedyne przepływy do przetestowania.

Wiedza o przepływach, które nie były widoczne na pierwszy rzut oka, pozwala dużo lepiej przetestować, działanie naszej skrzynki. Niestety wadą tej wiedzy jest to, że nasza skrzynka nie jest już taka czarna, a same testy stają się bardziej nietrwałe.

//given
final Component component = mock(Component.class);
final MainService mainSevice = new MainService(component);
//when
mainSevice.doSomething();
//then
final ArgumentCaptor<Test> captor = ArgumentCaptor.of(Test.class); 
verify(component).send(captor.capture());
final ObjectValue value = captor.getValue();
assertEquals(EXPECTED,value);

Tak więc tworzenie mocków, musisz mieć w małym palcu. Zazwyczaj jest to najbardziej pracochłonna rzecz przy pisaniu testów, ale jednocześnie jedna z najważniejszych.

Nadrabiaj Czytelnością Kodu

Jeżeli zdecydowałeś się na trochę większą jednostkę niż pojedyncza klasa, to musisz nadrabiać czytelnością kodu. Bo duża perspektywa sprawia, że przepływów do przetestowania jest dużo i łatwo się w tym zgubić.

By wiedzieć, co testujemy, musimy zadbać przede wszystkim o nazwy.

Na początek nadaj nazwy „magicznym wartościom”

//Mało czytelny test
test(){
   //given
   //when
   final int result = personService.calculate(30,40);
   //then
   assertEquals(5, result);
} 

//Tu już widać więcej
test(){
   //given
   final int ageRangeStart= 30;
   int ageRangeEmd= 40;
   //when
   final int numberOfPerson = personService.calculate(ageRangeStart, ageRangeEnd);
   //then
   assertEquals(EXPECTED_NUMBER_OF_PERSONS, numberOfPerson); 
}

Dbaj o dobre nazywanie metod testowych. Niech nazwa mówi jaki wynik będzie zwrócony dla pewnego zachowania. I nie zapominaj nazwać wartości. To ważne, bo gdy test przestanie działać, to nie będziesz wiedział, czy naprawiasz test, czy też naginasz jego działanie do akceptacji błędów.

//Niby coś wiadomo, ale nie za dużo
@Test
testFoUserValidation(){
   //...
} 

//Tu już wiemy czego się spodziewać
shouldReturnUnderageUserExceptionForUserWithAgeLessThan18(){
   //...
} 

Jak już zacząłeś pisać testy jednostkowe, to zauważyłeś, że zbiera się sporo danych testowych. Parametry testowe, oczekiwane wyniki. Warto wtedy podobne testy łączyć w jeden test parametryzowany.

@ParametrizedTest
@MethodSoruce("testData")
void prarametrizedTest(int ageRangeStart, int ageRangeEnd, int expected){
   //given
   //when
   final int numberOfPerson = personService.calculate(ageRangeStart, ageRangeEnd);
   //then
   assertEquals(expected, numberOfPerson); 
}

private static Object[] testData(){
   return new Object[]{
      {10, 20, 10},
      {20, 30, 125},
      {30, 40, 34},
      {40, 50, 14},
   }
}

Mógłbym tak jeszcze pisać i pisać o tej czytelności, ale większości są to te same zasady, które stosuje się w zwykłym kodzie. Tak w skrócie, to chodzi o to, byś dbał o kod testów, równie dobrze, jak o kod właściwy. Licz się z tym, że jeżeli będziesz chciał zapewnić dobre pokrycie, to testów będzie dużo. Warto się w nich jakoś odnajdywać.

Czy twój Kod jest Testowalny?

Jeżeli twój kod to bagno, które wciąga kolejnych niczego nieświadomych młodych zapalonych programistów i bez skrupułów wysysa z nich życie, niszczy ich kręgosłupy i dodaje do ich postaci kolejne punkty introwetyzmu.

To zmień pracę!!! No, albo zastań bohaterem. Uwaga!!! Wielu poległo!!!

Najważniejsze dla testów jest zachowanie jakiegoś podziału na odpowiedzialności, wtedy łatwiej wykroić z systemu jednostkę testową.

Jeżeli twój podział na odpowiedzialności polega na trzymaniu wszystkich serwisów w jednym pakiecie.

To weź, idź stąd i wróć jak się nauczysz po co są pakiety!

Równie ważna jest możliwość łatwego wstrzykiwania zależności, w szczególności tych, których nie chcemy testować, lub nad którymi do końca nie panujemy.

Co gdy nie możemy łatwo wstrzykiwać zależności ?

Jeżeli pracujesz w zastanym kodzie. To nie masz innego wyjścia. Musisz używać refleksji do wstrzykiwania danych w prywatne pola. Nie możesz zacząć bezpiecznie re-faktorować kodu, skoro nie masz testów. To tragiczne, ale wymagane w takim przypadku.

@Test
public void shouldHaveReturnHasManyProblemsAsTrue() {
    //given
    final EverythingManager everythingManager= new EverythingManager();
    final EverythingHelper helper = mock(EverythingHelper.class);
    ReflectionTestUtils.setField(everythingManager, "helper", helper);
    //when
    final boolen hasManyProblems = everythingManager.hasManyProblems();
    //then
    assertTrue(hasManyProblems);
}

Na poprawę snu Polecam drastycznie Zwiększyć pokrycie Testami

Jeżeli chcesz wprowadzać w swoim kodzie zmiany lekką ręką, bez strachu i bez zastanawiania się, czy wszystko nadal działa, to musisz drastycznie podnieść pokrycie kodu testami. Najlepiej, żeby było to coś około 90% lub więcej.

Nie musisz wtedy już tyle testować ręcznie. To jest przecież jakaś totalna głupota, żeby wielokrotnie uruchamiać aplikację i przekilkiwać się przez okienka, tylko po to, by dowiedzieć się, że to, co zrobiłeś, nie działa.

Nie ma co się męczyć. Lepiej napisać test!

Smutne, ale prawdziwe, wszyscy robimy błędy. Jesteśmy tylko ludźmi, a czynnik biologiczny zawodzi w tej branży najczęściej. Tak więc piszcie testy jednostkowe!

Wiem, że tytuł jest clickbaitowy, ale może ten wpis chociaż trochę poprawi kondycję mentalną, tym osobą, które nie lubią pisać testów jednostkowych 🙂

Please follow and like us:
Skuteczna refaktoryzacja w 10 krokach!

Odbierz Darmowy Poradnik o Refaktoryzacji!

Poznaj kilka prostych technik i wprowadź nową jakość w swoim projekcie.

Dzięki za dołączenie do mojej listy.

Coś poszło nie tak :( Spróbuj jeszcze raz.