JUnit 5 w Javie - konfiguracja, testy i CI/CD

JUNIT TESTY JEDNOSTKOWE. Tutorial dla bystrzaków (testy jednostkowe Java w JUnit 5). Klawiatura z zielonym klawiszem "Test".

Napisano przez

Juliusz Król

Opublikowano

26 wrz 2026

Spis treści

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.

Diagram przedstawia architekturę JUnit 5, łączącą Vintage, Jupiter i Third Party z JUnit Platform, wspieraną przez IDE/Build Tools.

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.junit i org.junit.jupiter.api bez 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.

FAQ - Najczęstsze pytania

W Mavenie dodaj zależność junit-jupiter oraz upewnij się, że Surefire obsługuje JUnit Platform. W Gradle użyj testImplementation i dodaj useJUnitPlatform() w zadaniu testowym. Następnie uruchom mvn test albo gradle test i sprawdź raport, ponieważ zielony build może oznaczać, że nie znaleziono żadnych testów.

Jupiter służy do nowych testów i udostępnia między innymi adnotacje @Test, @BeforeEach oraz @ParameterizedTest. Vintage jest potrzebny tylko wtedy, gdy projekt nadal zawiera testy JUnit 3 lub JUnit 4, na przykład z importem org.junit.Test. Podczas migracji można uruchamiać oba modele na jednej Platformie, a po przeniesieniu testów usunąć Vintage.

Test powinien skupiać się na jednym zachowaniu, działać bez sieci i zewnętrznych usług oraz wykonywać się w milisekundach. Pomocny jest układ Arrange, Act, Assert: przygotuj dane, wykonaj operację i sprawdź wynik. Używaj opisowych nazw testów, takich jak shouldAddTwoNumbers, oraz dbaj o izolację scenariuszy.

Test parametryzowany sprawdza tę samą regułę dla wielu danych bez kopiowania niemal identycznych metod. Warto uwzględnić wartości typowe, graniczne i błędne, a nie przypadkowe przykłady. @MethodSource przydaje się przy bardziej złożonych danych, takich jak obiekty, kolekcje lub całe scenariusze dostarczane z kodu.

Szybkie testy jednostkowe, obejmujące między innymi czyste klasy i reguły biznesowe, uruchamiaj przy każdym commicie. Testy integracyjne dotyczące bazy danych, HTTP lub kolejek uruchamiaj przy pull requeście albo przed wdrożeniem, a testy end-to-end trzymaj w osobnym, rzadszym etapie. Adnotacja @Tag pozwala oznaczać grupy jako fast, integration lub slow.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

junit maven gradle parametryzacja migracja

Udostępnij artykuł

Juliusz Król

Juliusz Król

Nazywam się Juliusz Król i od 4 lat zajmuję się automatyzacją testów oraz zapewnieniem jakości oprogramowania. Moje zainteresowanie tymi tematami zrodziło się z potrzeby zrozumienia, jak technologia może wspierać procesy tworzenia oprogramowania i jak kluczowe jest zapewnienie jego wysokiej jakości. W swoich tekstach skupiam się na praktycznych rozwiązaniach, które pomagają innym w efektywnym wdrażaniu automatyzacji oraz w rozwiązywaniu problemów związanych z jakością. Dzięki mojemu doświadczeniu staram się przekazywać wiedzę w sposób przystępny i zrozumiały, porównując różne podejścia oraz analizując aktualne trendy w branży. Zawsze dbam o to, aby moje materiały były rzetelne, aktualne i pomocne dla czytelników, którzy pragną rozwijać swoje umiejętności w obszarze QA.

Napisz komentarz