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.
Spis treści
- 1 Testy jednostkowe
- 2 JUnit
- 3 JUnit pierwszy prosty test
- 4 Jak uruchomić testy jednostkowe JUnit❓
- 5 JUnit tutorial
- 6 JUnit Assert – asercja, czyli weryfikacja założeń
- 7 Testy jednostkowe – dobre praktyki 👌
- 8 Testy jednostkowe w praktyce
- 9 JUnit integracja z IDE Intellij IDEA
- 10 Cykl życia testów w JUnit
- 11 JUnit Maven – instalacja JUnit przez zależności maven
- 12 JUnit Parameterized – testy parametryzowane
- 13 Dodatkowe materiały do nauki (nie tylko testów) 📚
- 14 Testy jednostkowe – co dalej? 🤔
- 15 20+ BONUSOWYCH materiałów z programowania
Testy jednostkowe

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
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
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
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
Wiersz poleceń – testy jednostkowe JUnit
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 tutorial

JUnit
Przećwiczymy teraz w praktyce wykorzystanie testów jednostkowych na przykładzie usługi kalkulatora – kod usługi znajdziesz poniżej.
Mamy tutaj:
- jedną metodę
add
, która przyjmuje dwa argumenty typu String, - próbuje je zamienić na liczbę całkowitą,
- a następnie zwraca ich sumę.
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
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
Metoda assertEquals
jest przeciążona tak, że przyjmuje praktycznie dowolne typy argumentów.
Dodatkowo mamy do dyspozycji kilka innych analogicznych metod:
assertNotEquals
– negacjaassertEquals
,assertNull
orazassertNotNull
– do porównywania wartości z wartością pustą NULL,assertTrue
orazassertFalse
– 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 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) 📚
- Testowanie oprogramowania
- KierunekJava.pl – najlepszy polski newsletter o programowaniu w Java
- Darmowy kurs java
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
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!
3 Comments
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.
Cześć Arek! Post jest aktualny, a starsze wpisy staram się cyklicznie aktualizować (oczywiście o ile jest to możliwe).
Ciekawe informacje na początek. Dzięki!