Gdy kod rośnie, ręczne sprawdzanie każdej zmiany szybko przestaje wystarczać. JUnit 5 pomaga zamienić testy jednostkowe w powtarzalny proces, który można uruchamiać lokalnie, w IDE i automatycznie w potoku CI/CD. Pokażę, jak działa ten framework, jak go skonfigurować w Mavenie i Gradle, jak pisać czytelne testy oraz jak uniknąć błędów, które odbierają automatyzacji największą wartość.
Najważniejsze informacje o JUnit 5 w jednym miejscu
- Architektura składa się z Platformy, modelu Jupiter i silnika Vintage.
- Jupiter dostarcza adnotacje, asercje, testy parametryzowane i rozszerzenia.
- Maven i Gradle pozwalają uruchamiać testy automatycznie z linii poleceń oraz w CI/CD.
- Test jednostkowy powinien być szybki, niezależny i skupiony na jednym zachowaniu.
- Aktualność zależności ma znaczenie, ponieważ nowsza linia JUnit wymaga nowszego JDK.

Jak działa JUnit 5 i skąd bierze się jego elastyczność
JUnit 5 nie jest tylko zbiorem adnotacji do oznaczania metod testowych. To cała platforma uruchamiania testów na JVM, która rozdziela mechanizm wykonywania od modelu, w którym testy są pisane. Taki podział daje większą swobodę integracji z IDE, narzędziami budującymi i innymi frameworkami testowymi.
Trzy elementy platformy
| Element | Rola | Kiedy ma znaczenie |
|---|---|---|
| JUnit Platform | Uruchamia testy i udostępnia API silnika testowego. | Gdy testy są wykonywane przez Maven, Gradle, IDE lub własne narzędzie. |
| JUnit Jupiter | Nowy model programowania i rozszerzeń dla testów. | Praktycznie zawsze, gdy tworzysz nowe testy. |
| JUnit Vintage | Uruchamia starsze testy napisane dla JUnit 3 i JUnit 4. | Podczas stopniowej migracji starszego projektu. |
Najczęściej używanym elementem jest Jupiter. To właśnie on dostarcza adnotacje takie jak @Test, @BeforeEach czy @ParameterizedTest. Vintage traktuję raczej jako most migracyjny, a nie rozwiązanie dla nowych projektów, ponieważ utrzymywanie dwóch modeli testów zwiększa złożoność konfiguracji.
W 2026 roku oficjalna dokumentacja projektu pokazuje już także linię JUnit 6, której aktualne wydanie wymaga Javy 17 lub nowszej. Jeśli pracujesz w projekcie opartym na Javie 8, 11 albo 17 i potrzebujesz zgodności z wcześniejszym ekosystemem, wybór wersji 5.x nadal może być rozsądny. Najpierw sprawdź JDK, wersję Spring Boota lub innego frameworka oraz konfigurację CI, dopiero potem przypinaj zależności.
Konfiguracja w Mavenie i Gradle bez zbędnych zależności
Najprostsza droga prowadzi przez agregat junit-jupiter. Zawiera on podstawowe elementy potrzebne do pisania i uruchamiania testów, więc nie musisz ręcznie dodawać kilku osobnych modułów. W projekcie korzystającym z linii 5.x możesz użyć przykładowej konfiguracji poniżej.
Maven
org.junit.jupiter
junit-jupiter
5.14.4
test
Sama zależność nie zawsze wystarczy. Maven musi jeszcze korzystać z wersji Surefire obsługującej Platformę JUnit, szczególnie w starszych projektach. Objawem problemu jest sytuacja, w której kod testu kompiluje się poprawnie, ale narzędzie pokazuje zero znalezionych testów.
Gradle
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.14.4")
}
tasks.test {
useJUnitPlatform()
}
W Gradle najczęściej zapominana jest właśnie instrukcja useJUnitPlatform(). Bez niej zadanie testowe może nie uruchomić testów Jupiter, mimo że zależności znajdują się w projekcie. Po konfiguracji uruchom mvn test albo gradle test i sprawdź raport, zamiast zakładać, że zielony build oznacza wykonanie wszystkich testów.
Nie dodawaj Vintage tylko dlatego, że widzisz go w dokumentacji. Potrzebujesz go wyłącznie wtedy, gdy projekt rzeczywiście zawiera testy ze starymi importami, na przykład org.junit.Test. W nowym kodzie używaj importów z pakietu org.junit.jupiter.api, bo to od razu pokazuje, z jakiego modelu korzysta test.
Jak napisać dobry test jednostkowy
Dobry test nie polega na tym, że metoda ma adnotację @Test. Powinien jasno pokazywać scenariusz, oczekiwany rezultat i granicę odpowiedzialności testowanej klasy. Ja najczęściej zaczynam od układu Arrange, Act, Assert: przygotuj dane, wykonaj operację, sprawdź wynik.
class CalculatorTest {
private final Calculator calculator = new Calculator();
@Test
void shouldAddTwoNumbers() {
int result = calculator.add(2, 3);
assertEquals(5, result);
}
}
Ten przykład jest krótki, ale ma ważną cechę: nazwa testu mówi, jakie zachowanie sprawdzamy. Unikaj nazw typu test1() lub shouldWork(), bo po awarii nie wyjaśniają, co przestało działać. Czytelna nazwa skraca analizę błędu i pomaga traktować raport z testów jak dokumentację zachowania aplikacji.
Asercje dopasowane do sytuacji
Do prostych porównań wystarczy assertEquals, ale biblioteka oferuje więcej narzędzi. assertTrue i assertFalse sprawdzają warunki logiczne, assertNull i assertNotNull obecność wartości, a assertThrows pozwala zweryfikować wyjątek wraz z jego typem.
@Test
void shouldRejectNegativeAmount() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> calculator.withdraw(-10)
);
assertEquals("Amount must be positive", exception.getMessage());
}
Sprawdzanie samego typu wyjątku bywa zbyt słabe. Jeśli komunikat jest częścią kontraktu, testuj go również, ale nie rób tego bez potrzeby, gdy tekst może się zmieniać bez wpływu na zachowanie. Przy kilku niezależnych warunkach użyj assertAll, aby zobaczyć wiele błędów w jednym uruchomieniu zamiast poprawiać test etapami.
Cykl życia i izolacja
@BeforeEach wykonuje się przed każdym testem, a @AfterEach po nim. @BeforeAll i @AfterAll dotyczą całej klasy, dlatego nadają się do kosztownego przygotowania współdzielonego zasobu, choć trzeba uważać na współdzielony stan.
Domyślnie każda metoda testowa otrzymuje nową instancję klasy testowej. To pomaga utrzymać izolację testów. Jeżeli testy zaczynają zależeć od kolejności wykonywania albo od zmiennych pozostawionych przez poprzedni scenariusz, problemem zwykle nie jest framework, tylko zbyt mocne sprzężenie testów.
Parametryzacja i rozszerzenia przydatne w automatyzacji
Powtarzanie niemal identycznych metod testowych szybko prowadzi do bałaganu. Test parametryzowany pozwala uruchomić ten sam scenariusz dla wielu danych, dzięki czemu jedna reguła jest sprawdzana szerzej, ale bez kopiowania kodu.
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"10, 5, 15",
"0, 7, 7"
})
void shouldAddNumbers(int first, int second, int expected) {
assertEquals(expected, calculator.add(first, second));
}
Ważne jest, aby zestaw danych miał sens biznesowy. Trzy przypadkowe liczby nie dają dużej pewności, natomiast kombinacja wartości typowych, granicznych i błędnych potrafi szybko ujawnić lukę w implementacji. Do bardziej złożonych danych użyj @MethodSource, który pozwala dostarczać obiekty, kolekcje lub całe scenariusze z kodu.
Adnotacja @Nested pomaga grupować testy według stanu lub funkcji, a @Tag umożliwia oznaczanie zestawów, na przykład jako fast, integration albo slow. W CI/CD możesz wtedy uruchamiać szybkie testy przy każdym commicie, a cięższe scenariusze przed scaleniem zmian.
Rozszerzenia zamiast własnych hacków
Model rozszerzeń pozwala podłączyć dodatkową logikę do cyklu życia testu. Zamiast tworzyć ręczne mechanizmy inicjalizacji, możesz użyć gotowego rozszerzenia do mocków, parametrów, zasobów tymczasowych lub integracji z innym narzędziem.
To miejsce, w którym łatwo przesadzić. Rozszerzenie powinno usuwać powtarzalny kod i poprawiać czytelność, a nie ukrywać połowę scenariusza za konfiguracją. Gdy po otwarciu klasy testowej nie wiadomo, skąd bierze się dane wejściowe, automatyzacja stała się mniej przejrzysta niż kod produkcyjny.
JUnit 5 w CI/CD i migracji ze starszych testów
Największa korzyść z testów pojawia się wtedy, gdy uruchamiają się automatycznie po każdej zmianie. Pipeline powinien kompilować projekt, wykonywać testy jednostkowe, generować raport i kończyć się błędem, gdy choć jeden wymagany scenariusz nie przejdzie.
| Warstwa | Przykładowe testy | Typowa częstotliwość |
|---|---|---|
| Szybkie testy jednostkowe | Czyste klasy, walidacja, reguły biznesowe | Przy każdym commicie |
| Testy integracyjne | Baza danych, HTTP, kolejki, konfiguracja | Przy pull requeście lub przed wdrożeniem |
| Testy end-to-end | Pełne przepływy użytkownika | Rzadziej, zwykle w osobnym etapie |
Nie uruchamiaj wszystkiego w jednej grupie bez oznaczeń. Test jednostkowy powinien działać w milisekundach i nie wymagać sieci, natomiast test integracyjny może potrzebować kontenera, bazy lub usługi zewnętrznej. Rozdzielenie tych klas daje krótszy feedback dla programisty i mniej losowych awarii w pipeline.
Przeczytaj również: Chrome XPath - Pisz stabilne locatory. Testuj skutecznie.
Na co uważać przy migracji z JUnit 4
Najczęstsza pułapka polega na mechanicznym zamienieniu importów bez sprawdzenia różnic w cyklu życia i rozszerzeniach. W Jupiterze zamiast @Before używa się @BeforeEach, zamiast @BeforeClass adnotacji @BeforeAll, a reguły JUnit 4 często wymagają nowego odpowiednika opartego na rozszerzeniu.
-
Nie mieszaj importów z
org.junitiorg.junit.jupiter.apibez wyraźnego powodu. - Nie zakładaj zgodności wszystkich reguł i runnerów ze staryjskiego modelu.
- Nie przenoś bezrefleksyjnie statycznego współdzielonego stanu do nowych testów.
- Nie traktuj pokrycia kodu jako dowodu jakości, bo wysoki procent nie gwarantuje dobrych scenariuszy.
W dużym repozytorium migrację najlepiej prowadzić etapami. Najpierw uruchom stare i nowe testy na jednej Platformie, potem przenoś klasy według modułów i dopiero na końcu usuwaj Vintage. Dzięki temu zespół nie musi wykonywać ryzykownego przełączenia całego projektu w jednym kroku.
Jak zacząć, żeby testy naprawdę pomagały
Na początek wybierz jedną klasę z logiką biznesową, która nie komunikuje się bezpośrednio z bazą danych ani siecią. Napisz dla niej kilka testów opisujących normalny przypadek, wartość graniczną i niepoprawne dane. Taki mały zestaw pokaże więcej niż kilkadziesiąt automatycznie wygenerowanych metod bez jasnych oczekiwań.
W codziennej pracy najbardziej opłaca się pilnować trzech rzeczy: niezależności testów, jednoznacznych nazw oraz szybkiego uruchamiania. Framework daje rozbudowane możliwości, ale nie zastąpi dobrego projektu kodu. Jeżeli test wymaga wielu mocków, skomplikowanego przygotowania i znajomości kolejności wywołań, prawdopodobnie warto uprościć także klasę produkcyjną.
Najlepszy efekt daje połączenie testów jednostkowych z automatycznym uruchamianiem w CI/CD i osobnym zestawem testów integracyjnych. Wtedy JUnit przestaje być tylko biblioteką do sprawdzania metod, a staje się stałym mechanizmem ochrony jakości podczas rozwoju aplikacji.