JUnit – Testy jednostkowe » tutorial dla bystrzaków (testy jednostkowe Java w JUnit 5)

Czy wiesz, dlaczego zawodowi programiści z kilkuletnim doświadczeniem popełniają tak mało błędów i są w stanie wypuszczać nowe wersje swojej aplikacji, nawet kilka razy dziennie i jednocześnie niczego w niej nie popsuć?
Magia? – No raczej niekoniecznie…😉

W dużej mierze jest to związane oczywiście z ich doświadczeniem i obyciem, jakie zdobyli przez te lata – jednak to nie wszystko. Bez wątpienia ważny jest również sam proces testowania, któremu poddawana jest aplikacja. Bez odpowiednich testów możemy jedynie mieć nadzieję i mocno trzymać kciuki, żeby nasz kod, który wytworzyliśmy, działał zgodnie z założeniami. Jednak zapewne jak się domyślasz, w zawodowym programowaniu nie do końca o to chodzi…

To jak? Chcesz wiedzieć więcej? 👋 💬

Jest to już drugi wpis z serii o testowaniu aplikacji. W poprzedniej części: ➡ Testowanie oprogramowania zrobiliśmy wprowadzenie do testowania z punktu widzenia developera, omówiliśmy typy testów i korzyści, jakie wynikają z ich stosowania. Jeżeli jeszcze nie czytałeś tego wpisu, to koniecznie nadrób zaległości – najlepiej jeszcze przed tym tekstem.


Testy jednostkowe

Piramida testów

Piramida testów

Testy jednostkowe są na najniższym poziomie zdrowej piramidy testów.

Co za tym idzie:

  • powinno być ich stosunkowo najwięcej w projekcie,
  • ich koszt przygotowania i utrzymania powinien być relatywnie najniższy
  • oraz powinny działać najszybciej.

W praktyce oznacza to, że testy jednostkowe są bardzo blisko kodu źródłowego naszej aplikacji. W poszczególnym teście jednostkowym testujemy pojedynczą klasę, czy nawet pojedynczą metodę.


JUnit

JUnit 5

JUnit 5

Dzisiaj skupimy się na najpopularniejszej obecnie bibliotece do testów jednostkowy oraz integracyjnych – JUnit.

Z tego tekstu dowiesz się:

  • co to są testy jednostkowe oraz dlaczego jako programista po prostu MUSISZ się nimi zainteresować;
  • do czego służy biblioteka JUnit oraz jak wykorzystać ją w Twoim projekcie,
  • oraz poznasz praktyczne przykłady, jak wprowadzić w życie dobre praktyki testowania jednostkowego kodu.

JUnit pierwszy prosty test

Pojedynczy test jednostkowy w JUnit to metoda oznaczona adnotacją @Test.

import org.junit.jupiter.api.Test;
public class ExamplesTest {
@Test
void emptyTestMethod(){
}
}

Pamiętamy również o odpowiednim imporcie: import org.junit.jupiter.api.Test .

W naszym przykładzie do organizacji projektu wykorzystałem Maven, dlatego klasa zawierająca metodę testującą (przy zachowaniu domyślnej konfiguracji), powinna być w katalogu src/test/java .

Testy jednostkowe maven

Testy jednostkowe Maven

Mamy już pierwszą metodę testową – co prawda, jeszcze nic nie robi, ale i na to przyjdzie pora. Pusta metoda testująca, z technicznego punktu widzenia, jest również poprawnym testem – ponieważ nie określiliśmy żadnych warunków, które mają być spełnione, to taki test zawsze będzie zwracał informację, że wszystko jest 🆗. ✅


Jak uruchomić testy jednostkowe JUnit❓

Uruchommy teraz nasze testy.

Ja podczas pracy najczęściej wykorzystuję do tego IDE Intellij, jednak możliwości w tej kwestii jest całkiem sporo.

Testy jednostkowe Intellij

Testy jednostkowe Intellij

Testy jednostkowe Intellij

Testy jednostkowe Intellij

W Intellij IDEA możemy to zrobić, między innymi korzystając z:

  • menu kontekstowego kodu źródłowego, z którego poziomu możemy uruchomić wybraną metodę testową lub wszystkie metody z danej klasy;
  • niewielkich zielonych trójkącików, które znajdziesz przy nazwie metody lub klasy. Działają one analogicznie jak menu kontekstowe;
  • menu górnego Run, gdzie możemy zdefiniować np. przekazywane argumenty do maszyny wirtualnej Java podczas uruchomienia testów
  • oraz menu kontekstowego widoku Projekt, gdzie możemy np. uruchomić wszystkie testy z danego pakietu.

Niezależnie od sposobu, który wybierzesz – IDE powinno uruchomić wskazane testy i wyświetlić Ci mniej więcej taki raport z ich przebiegu.

JUnit Intellij IDEA

JUnit Intellij IDEA

Wiersz poleceń – testy jednostkowe JUnit

Kolejnym sposobem jest uruchomienie testów z poziomu linii komend.
Z tego rozwiązania najczęściej korzystam podczas przygotowywania skryptów pod CI (Continuous Integration), np. w Bamboo, Github Actions lub Jenkins.
Do uruchomienia wszystkich testów z danego projektu wystarczy uruchomienie poniższej komendy maven.
mvn test

W razie potrzeby możemy też doprecyzować, które testy chcemy uruchomić, np. mvn test -Dtest=ExamplesTest .

Na koniec powinniśmy zobaczyć raport z przebiegu Twoich testów.

JUnit konsola

JUnit konsola


JUnit tutorial

JUnit

JUnit

Przećwiczymy teraz w praktyce wykorzystanie testów jednostkowych na przykładzie usługi kalkulatora – kod usługi znajdziesz poniżej.

Mamy tutaj:

public class CalculatorService {
public int add(String a, String b) {
if (a == null || b == null) {
throw new IllegalArgumentException("Arguments 'a' and 'b' are required.");
}
return Math.addExact(Integer.parseInt(a), Integer.parseInt(b));
}
}

Teoretycznie bardzo prosta metoda – ot zwykły kalkulator. Jednak chcąc dobrze przetestować, nawet tak krótki fragment kodu, musimy uwzględnić bardzo dużo warunków, np.

  • tak zwany happy path, czyli podstawowy („szczęśliwy”) przepływ metody, gdy wszystko idzie zgodnie z planem,
  • warunki brzegowe, czyli bardziej problematyczne przypadki, np.:
    • walidację na przekazanie do metody niepoprawnych argumentów (do pierwszego argumentu, do drugiego oraz do obu jednocześnie),
    • co się stanie, gdy do metody zostanie przekazana wartość pusta NULL,
    • dodawanie wartości ujemnych,
    • bardzo duże wartości, które spowodują przekroczenie zakresu typu Integer,
    • itp. itd.

Jak widzisz, możliwości jest całkiem sporo. Jeżeli chcemy mieć pewność, że nasz kod zachowuje się w każdej z tych sytuacji zgodnie z naszymi oczekiwaniami, to musimy ustalić te warunki, a następnie przygotować testy z odpowiednimi asercjami, które je zweryfikują.

➡ ZOBACZ 👉:


JUnit Assert – asercja, czyli weryfikacja założeń

Dzięki asercjom jesteśmy w stanie weryfikować nasze założenia wobec kodu.

  • Poczynając od bardzo prostych, takich jak sprawdzenie, czy metoda zwróciła konkretną wartość liczbową,
  • do bardziej złożonych, takich jak sprawdzenie, czy zwrócony obiekt jest danego typu,
  • czy ostatecznie sprawdzenie, czy metoda rzuciła oczekiwany wyjątek.

Podstawowa asercja to wywołanie metody:
public static void assertEquals(int expected, int actual)
z klasy: org.junit.jupiter.api.Assertions .

class CalculatorServiceAddTest {
@Test
void shouldAddTwoIntegers() {
int result = calculatorService.add("10", "20");
Assertions.assertEquals(30, result);
}
}

Jako pierwszy argument do metody assertEquals przekazujemy wynik, którego się spodziewamy, a jako drugi nasz aktualny wynik, jaki zwróciła metoda.

Jeżeli wszystko pójdzie zgodnie z założeniami, to program nie podejmie żadnej akcji i przejdzie do weryfikacji kolejnej asercji lub zakończy cały test, jeżeli to była ostatnia asercja.

W przypadku kiedy wynik nie będzie się zgadzał z oczekiwaniami, działanie programu zostanie przerwane (nie będą sprawdzane kolejne warunki!) i zobaczymy stosowny komunikat.

JUnit Assert

JUnit Assert

Błąd org.opentest4j.AssertionFailedError przerywa aktualnie wykonywany test i oznacza go jako niepoprawny.
W szczegółach błędu możesz doczytać, jaki jest dokładny powód niepowodzenia testu.

JUnit Assertions – assertEquals

JUnit Assertions – assertEquals

Metoda assertEquals jest przeciążona tak, że przyjmuje praktycznie dowolne typy argumentów.
Dodatkowo mamy do dyspozycji kilka innych analogicznych metod:

  • assertNotEquals – negacja assertEquals ,
  • assertNull oraz assertNotNull – do porównywania wartości z wartością pustą NULL,
  • assertTrue oraz assertFalse – do porównywania wartości logicznych boolean,
  • assertArrayEquals – do porównywania tablic,
  • oraz metoda fail – której każde wywołanie automatycznie oznacza cały test jako niepoprawny.

Mimo iż zbiór asercji dostarczanych w pakiecie z JUnit jest całkiem pokaźny, to w praktyce bardzo często korzysta się z zewnętrznych bibliotek, które jeszcze rozszerzają ich możliwości.
Dwie obecnie najpopularniejsze biblioteki tego typu to Hamcrest oraz AssertJ – osobiście korzystam głównie z AssertJ.

AssertJ

Jeżeli już korzystamy z mavena, to dodanie biblioteki AssertJ do naszego projektu będzie wiązało się tylko z dodaniem nowej zależności.

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.20.2</version>
<scope>test</scope>
</dependency>

Od tego momentu w naszych testach możemy już korzystać z dużo bardziej rozbudowanych assercji.

Na uwagę zasługuje przede wszystkim prostota i genialna wręcz użyteczność metody: Assertions.assertThat z pakietu: org.assertj.core.api.Assertions .

Metoda assertThat przeciążona jest z wykorzystaniem większości podstawowych typów i dla każdego z nich udostępnia zbiór specyficznych metod.
Przykładowo – jeżeli przekażemy do niej argument typu String, to będziemy mieli dostępną np. metodą: isNotEmpty() .

String string = "Java";
assertThat(string).isNotEmpty();

Natomiast jeżeli jako argument przekażemy kolekcję, to możemy już weryfikować konkretne elementy tej kolekcji. Co w praktyce jest bardzo wygodne.

List<String> list = Arrays.asList("Ala", "zna", "Javę");
assertThat(list).containsExactly("Ala", "zna", "Javę");

Testy jednostkowe – dobre praktyki 👌

Przyjrzyjmy się teraz kilku najczęściej występującym dobrym praktykom związanym z testami jednostkowymi.

Zasady testowania jednostkowego FIRST

  • Fast – szybkie testy uruchamiane szybko, najlepiej bezpośrednio po dokonaniu zmian w kodzie,
  • Independent (Isolated) – każdy test powinien być niezależny i działać w izolacji od pozostałych. Nie może być sytuacji, w których działanie testu zależne jest od wcześniejszego wykonania innego testu,
  • Repeatable – stabilne wyniki przy każdym kolejnym uruchomieniu. Niezależnie, czy test przechodzi, czy też nie – jego wynik musi być stabilny. W przeciwnym wypadku nie będziemy mogli polegać na takich wynikach,
  • Self-checking – jednoznaczna i automatyczna informacja o niepowodzeniu testu. Korzystanie z asercji niejako automatycznie spełnia ten warunek. Nie może być sytuacji, w której do odkreślenia, czy warunki zostały spełnione, czy też niezbędne jest ręczne działanie człowieka,
  • Timely – aktualne oraz napisane w tym samym czasie co kod. Test, który jest nieaktualny i który nie sprawdza tego co powinien, jest niewiele wart.

JUnit – struktura testu (nie tylko) jednostkowego

Dość powszechną oraz polecaną praktyką jest dzielenie poszczególnych przypadków testowych na trzy sekcje:

  • given – przygotowanie wszystkich danych wejściowych,
  • when – uruchomienie testowanej metody,
  • then – weryfikacja otrzymanych wyników.
@Test
void shouldAddTwoIntegers() {
// given
String a = "10";
String b = "20";
// when
int result = calculatorService.add(a, b);
// then
assertEquals(30, result);
}

Można w tym celu posiłkować się różnymi innymi rozwiązaniami, jak np. Cucumber, jednak jeżeli korzystamy z JUnit, to wystarczy w tym celu oznaczyć poszczególne sekcje stosownym komentarzem.

Nie jest to wymóg techniczny, a jedynie dobra praktyka, która poprawia czytelność naszych testów.


Testy jednostkowe w praktyce

Podsumujemy teraz zdobytą już przez nas wiedzę i dokończmy testowanie przykładowego kalkulatora.
W poniższym fragmencie kodu zawarłem wszystkie omawiane wcześniej przypadki testowe.

Zwróć uwagę, że

  • każdy przypadek testowy został zamodelowany jako osobna metoda z adnotacją @Test ,
  • nazwą metody, która jednoznacznie sugeruje, jakie mamy oczekiwania wobec tego przypadku,
  • oraz z wyraźnym podziałem na trzy sekcje: given, when, then.

W poszczególnych asercjach posiłkowałem się biblioteką AssertJ.

public class CalculatorServiceTest {
private CalculatorService calculatorService;
@BeforeEach
void beforeEach() {
calculatorService = new CalculatorService();
}
@Test
void shouldAddTwoCorrectNumbers() {
// given
String a = "10";
String b = "20";
// when
int result = calculatorService.add(a, b);
// then
assertEquals(30, result);
}
@Test
void shouldThrowExceptionOnInvalidFirstArgument() {
// given
String a = "wrong-number";
String b = "10";
// when
Throwable throwable = catchThrowable(() -> calculatorService.add(a, b));
// then
assertThat(throwable)
.isInstanceOf(NumberFormatException.class)
.hasMessage("For input string: \"wrong-number\"");
}
@Test
void shouldThrowExceptionOnInvalidSecondArgument() {
// given
String a = "10";
String b = "wrong-number";
// when
Throwable throwable = catchThrowable(() -> calculatorService.add(a, b));
// then
assertThat(throwable)
.isInstanceOf(NumberFormatException.class)
.hasMessage("For input string: \"wrong-number\"");
}
@Test
void shouldThrowExceptionOnEmptyFirstArgument() {
// given
String a = "";
String b = "10";
// when
Throwable throwable = catchThrowable(() -> calculatorService.add(a, b));
// then
assertThat(throwable)
.isInstanceOf(NumberFormatException.class)
.hasMessage("For input string: \"\"");
}
@Test
void shouldThrowExceptionOnIntegerOverFlow() {
// given
String a = Integer.MAX_VALUE + "";
String b = "11";
// when
Throwable throwable = catchThrowable(() -> calculatorService.add(a, b));
// then
assertThat(throwable)
.isInstanceOf(ArithmeticException.class)
.hasMessage("integer overflow");
}
}

JUnit integracja z IDE Intellij IDEA

Z testów jednostkowych możemy oczywiście korzystać z poziomu linii komend i notatnika, jednak posiłkując się dowolnym IDE, zapewne będzie nam się pracowało dużo sprawniej.

Podstawowa integracja, której na pewno warto się przyjrzeć to przynajmniej:

  • generowanie raportów z wyniku testów,
  • uruchamianie poszczególnych przypadków testowych całych klas, pakietów itp.,
  • oraz generowanie samych klas testowych.

Korzystając z automatycznych generatorów (żółta żarówka, która pojawi się po najechaniu na nazwę klasy) można wygenerować szablon klasy testowej na podstawie klasy, którą chcemy testować.


Cykl życia testów w JUnit

JUnit oferuje nam również wiele opcji jeżeli chodzi o możliwość wpłynięcia na przebieg naszych testów w różnych momentach ich cyklu życia.
Możemy np. uruchomić stosowny fragment kodu po (lub przed) każdej poszczególnej metodzie testującej oraz przed (lub po) wszystkimi naszymi metodami.
Co niewątpliwie jest bardzo praktyczne, kiedy np. chcemy przygotować i później posprzątać środowisko, żeby przeprowadzić nasz test.

public class TaskJUnit {
@BeforeAll
static void beforeAll() {
System.out.println("beforeAll");
}
@BeforeEach
void beforeEach() {
System.out.println("beforeEach");
}
@Test
void testMethod1() {
System.out.println("testMethod1");
}
@Test
void testMethod2() {
System.out.println("testMethod2");
}
@AfterEach
void afterEach() {
System.out.println("afterEach");
}
@AfterAll
static void afterAll() {
System.out.println("afterAll");
}
}

Po uruchomieniu powyższego kodu widzimy, w jakiej kolejności zostaną uruchomione poszczególne metody.

JUnit – cykl życia testu

JUnit beforeAll

Metoda zostanie uruchomiona PRZED wszystkimi metodami testowymi w danej klasie – a co za tym idzie, musi zostać oznaczona jako statyczna.
Niezależnie od ilości testów w ramach naszej klasy z testami, taka metoda zostanie uruchomiona tylko raz.

JUnit afterAll

Metoda zostanie uruchomiona PO wszystkich metodach testowych w danej klasie – a co za tym idzie, musi zostać oznaczona jako statyczna.
Niezależnie od ilości testów w ramach naszej klasy z testami, taka metoda zostanie uruchomiona tylko raz.

JUnit beforeEach

Metoda zostanie uruchomiona PRZED każdą metodą testową w obrębie danej klasy. Metoda zostanie uruchomiona tyle razy ile mamy takich metod.

JUnit after Each

Metoda zostanie uruchomiona PO każdej metodzie testowej w obrębie danej klasy. Metoda zostanie uruchomiona tyle razy ile mamy takich metod.


JUnit Maven – instalacja JUnit przez zależności maven

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>${junit.platform.version}</version>
</dependency>	

JUnit Parameterized – testy parametryzowane

JUnit oferuje wiele różnych rozszerzeń, a jednym z popularniejszych i bardziej przydatnych są z pewnością testy parametryzowane.
Ich wykorzystanie pomaga w rozdzieleniu przygotowania danych testowych od samych testów. Z pewnością jest to szczególnie przydatne gdy chcemy np. przeprowadzić bardzo podobne testy, ale dla całkowicie różnych danych wejściowych.

Jeżeli to jest Twój przypadek, to warto zainteresować się takimi adnotacjami jak @ParameterizedTest oraz @ValueSource i @MethodSource .

@ParameterizedTest
@ValueSource(strings = {"Arg-1", "Arg-2", "Arg-3"})
void valueSource(String param) {
System.out.println(param);
}
@ParameterizedTest
@EnumSource(value = DayOfWeek.class)
void enumSource(DayOfWeek day) {
System.out.println(day);
}
@ParameterizedTest
@MethodSource("stringProvider")
void methodSource(String param) {
System.out.println(param);
}
static Stream<String> stringProvider() {
return Stream.of("Ala", "zna", "Javę");
}

Dodatkowe materiały do nauki (nie tylko testów) 📚


Testy jednostkowe – co dalej? 🤔

Uff. 🙂 To było całkiem niezłe wprowadzenie do testów jednostkowych i mam nadzieję, że bazując na zgromadzonych tutaj przykładach uda Ci się wprowadzić testy w Twojej aplikacji. Do czego swoją drogą gorąca zachęcam, bo jest to jedna z lepszych inwestycji, jakie możemy zrobić dla naszych aplikacji.

Czy to koniec informacji, jakie warto poznać w kontekście testowania kodu?

Zdecydowanie NIE! Tak naprawdę to dopiero początek… 🙂
Jako kolejny krok proponuję zapoznać się z mockami, TDD oraz integracją testów z cyklem życia naszych aplikacji (CI).

Jeżeli interesują Cię takie tematy, to zapraszam na KierunekJava.pl, gdzie w cotygodniowym newsletterze staram się rozłożyć Javę na czynniki pierwsze! 💪

Pozdrawiam i do usłyszenia!
Tomek


Wielka prośba i… PREZENT! 🎁

Bardzo dużo pracy włożyliśmy w przygotowanie tego materiału, dlatego mam prośbę – pomóż mi dotrzeć do innych osób, którym ten materiał może pomóc.
Może wśród znajomych masz kogoś, komu warto podesłać ten wpis?
Z góry bardzo, bardzo dziękuję!

W podziękowaniu za to, że jesteś ze mną, przygotowałem dla Ciebie prezent. 😉

  • Kod źródłowy omawianego projektu z przykładami w JUnit 5
  • oraz streszczenie najważniejszych rzeczy o JUnit i testach jednostkowych w jednym miejscu, w pliku PDF.

Wystarczy, że odpowiesz na jedno proste pytanie w komentarzu pod tym wpisem na FB.

>> Wpis na FB <<

Za co lubisz (lub nie lubisz) testów i jakie są Twoje doświadczenia w tym temacie?

UWAGA: Żeby dostać bonus, trzeba dodać komentarz bezpośrednio pod wpisem na FB, a w treści musi znajdować się tekst #KierunekJava
Bezpośrednio po zostawieniu komentarza, skontaktuje się z Tobą nasz asystent – bot. 😉

 

 


Jak zostać programistą

8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista.

Jak zostać programistą
2 komentarze
Share:

2 Comments

  1. Arek says:

    Przydany wpis. Prośba jedynie o zamieszczanie przy postach daty publikacji.
    Często na blogach mi osobiście tego brakuje z uwagi na fakt, że mocno archiwalne materiały staram się w pierwszej kolejności omijać z uwagi na ryzyko nieaktualności.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Chcesz wejść do IT lub zmienić branżę i zostać programistą?

Skorzystaj z DARMOWEJ WIEDZY o Javie! >> KierunekJava.pl

Lista 8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista!

Dołączam do newslettera
i odbieram materiały!

PAMIĘTAJ, żeby odebrać wiadomość potwierdzającą i kliknąć w przycisk.


Zapisując się na newsletter, zgadzasz się na przetwarzanie Twoich danych osobowych w celu wysyłania na wskazany przez Ciebie adres e-mail informacji handlowych o nowościach, promocjach, produktach i usługach związanych z serwisami stormit.pl i kierunekprogramista.pl. Będzie to marketing bezpośredni. Administratorem Twoich danych osobowych będzie Tomasz Woliński prowadzący działalność gospodarczą Tomasz Woliński Storm IT, Przytulna 38/43, 80-176 Gdańsk, NIP: 7431875586. Przysługuje Ci prawo do cofnięcia zgody, żądania wglądu do Twoich danych, wniesienia sprzeciwu co do ich przetwarzania, sprostowania, usunięcia i ograniczenia przetwarzania. Więcej informacji o tym jak przetwarzam Twoje dane znajdziesz na stormit.pl/polityka-prywatnosci/.