Pytania rekrutacyjne JUnit 5

55 pytań — 20 odpowiedzi dostępnych od razu

Pytania rekrutacyjne JUnit 5 sprawdzają, czy potrafisz projektować testy, które są zrozumiałe i odporne na zmiany w kodzie. Darmowy podgląd zawiera przykładowe pytania z zestawu 55 zagadnień.

Materiał obejmuje adnotacje, asercje, lifecycle testów oraz testy parametryzowane. To dobra powtórka dla programistów Java, którzy chcą pewniej rozmawiać o jakości i automatyzacji testów.

Darmowy start

Pierwsze 20 odpowiedzi są dostępne od razu

Przeglądaj pełną listę 55 pytań. Pełne odpowiedzi, przykłady kodu i materiały do nauki odblokujesz w panelu.

Odblokuj 35 odpowiedzi

Podstawy JUnit 5

Czym jest JUnit 5 i jakie są jego główne cele projektowe?

Odpowiedź w 30 sekund: JUnit 5 to nowoczesny framework testowy dla aplikacji Java, wydany w 2017 roku jako następca JUnit 4. Jego główne cele projektowe to modułowość, wsparcie dla Javy 8+ (lambdy, strumienie), rozszerzalność poprzez model rozszerzeń (Extension API) oraz zapewnienie stabilnej platformy dla uruchamiania różnych frameworków testowych na JVM.

Odpowiedź w 2 minuty: JUnit 5 powstał jako odpowiedź na ograniczenia JUnit 4, który ze względu na monolityczną architekturę i wsparcie dla starszych wersji Javy nie nadążał za nowoczesnymi praktykami programistycznymi. Projekt został sfinansowany przez kampanię crowdfundingową "JUnit Lambda" i zaprojektowany od podstaw z myślą o modułowości oraz lepszym wsparciu dla funkcjonalności Javy 8+.

Główne cele projektowe JUnit 5 to: (1) Modułowość - rozdzielenie API testowego od silnika uruchomieniowego, co umożliwia współistnienie wielu frameworków testowych; (2) Rozszerzalność - elastyczny model rozszerzeń (@ExtendWith) zastępujący sztywne @Runner i @Rule z JUnit 4; (3) Wsparcie nowoczesnej Javy - wykorzystanie wyrażeń lambda, strumieni, typów funkcyjnych; (4) Bogata diagnostyka - czytelne asercje, możliwość grupowania (assertAll), wsparcie dla wyjątków (assertThrows); (5) Programowanie deklaratywne - intuicyjne adnotacje jak @DisplayName, @Nested, @Tag ułatwiające organizację testów.

JUnit 5 wspiera testy parametryzowane natywnie (@ParameterizedTest), dynamiczne (@TestFactory), zagnieżdżone (@Nested) oraz pozwala na pisanie własnych rozszerzeń realizujących cross-cutting concerns (np. mockowanie, mierzenie czasu, zarządzanie cyklem życia).

Przykład kodu:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Testy kalkulatora")
class KalkulatorTest {

    private Kalkulator kalkulator;

    @BeforeEach
    void przygotujKalkulator() {
        // Tworzenie nowej instancji przed każdym testem
        kalkulator = new Kalkulator();
    }

    @Test
    @DisplayName("Dodawanie dwóch liczb całkowitych")
    void dodawanieLiczb() {
        // Asercja z czytelnym komunikatem błędu
        assertEquals(5, kalkulator.dodaj(2, 3),
            "Suma 2 + 3 powinna wynosić 5");
    }

    @Test
    @DisplayName("Dzielenie przez zero rzuca wyjątek")
    void dzielenieprzezZero() {
        // Sprawdzenie rzucanego wyjątku - nowość w JUnit 5
        ArithmeticException wyjatek = assertThrows(
            ArithmeticException.class,
            () -> kalkulator.podziel(10, 0)
        );
        assertTrue(wyjatek.getMessage().contains("zero"));
    }
}

Materiały

Z jakich modułów składa się architektura JUnit 5 (Jupiter, Vintage, Platform)?

Odpowiedź w 30 sekund: JUnit 5 składa się z trzech głównych podprojektów: JUnit Platform (fundament uruchomieniowy na JVM), JUnit Jupiter (nowe API testowe i silnik dla JUnit 5) oraz JUnit Vintage (silnik kompatybilny z testami JUnit 3 i JUnit 4). Taka modułowa architektura pozwala na uruchamianie różnych frameworków testowych na wspólnej platformie.

Odpowiedź w 2 minuty: JUnit Platform stanowi fundament całej architektury - definiuje API TestEngine oraz zapewnia infrastrukturę do uruchamiania testów na JVM. Platforma obsługuje odkrywanie testów (Launcher API), raportowanie wyników oraz integrację z narzędziami budującymi (Maven, Gradle) i IDE (IntelliJ, Eclipse, VS Code). To dzięki niej różne frameworki testowe mogą współistnieć w jednym projekcie.

JUnit Jupiter to właściwy framework testowy w wersji 5 - dostarcza nowe API do pisania testów (adnotacje @Test, @BeforeEach, asercje, rozszerzenia) oraz silnik JupiterTestEngine, który implementuje API platformy. To w Jupiterze znajdują się nowoczesne funkcje jak testy parametryzowane, dynamiczne, zagnieżdżone i model rozszerzeń.

JUnit Vintage zapewnia kompatybilność wsteczną - jego silnik VintageTestEngine umożliwia uruchamianie istniejących testów napisanych w JUnit 3 i JUnit 4 na platformie JUnit 5. Dzięki temu można migrować projekt stopniowo, bez konieczności przepisywania wszystkich testów jednocześnie.

flowchart TB
    subgraph "Test Authors & Tools"
        A[Testy Jupiter<br/>@Test JUnit 5]
        B[Testy Vintage<br/>@Test JUnit 4]
        C[Inne testy<br/>Spock, Cucumber, ...]
        T[Narzędzia: Maven, Gradle, IDE]
    end

    subgraph "Test Engines"
        E1[JupiterTestEngine]
        E2[VintageTestEngine]
        E3[Inne silniki<br/>3rd party]
    end

    subgraph "Fundament"
        P[JUnit Platform<br/>Launcher + TestEngine API]
    end

    A --> E1
    B --> E2
    C --> E3
    T --> P
    E1 --> P
    E2 --> P
    E3 --> P

Przykład kodu:

// Zależności w pom.xml pokazują strukturę modułów:
//
// junit-jupiter-api      -> API do pisania testów (adnotacje, asercje)
// junit-jupiter-engine   -> silnik Jupiter (implementacja TestEngine)
// junit-jupiter-params   -> wsparcie testów parametryzowanych
// junit-vintage-engine   -> silnik dla starych testów JUnit 4
// junit-platform-launcher -> API do programowego uruchamiania testów

import org.junit.jupiter.api.Test;       // z modułu jupiter-api
import org.junit.jupiter.params.ParameterizedTest; // z jupiter-params

class ArchitekturaTest {

    @Test
    void testNowegoStylu() {
        // Ten test uruchomi JupiterTestEngine
    }
}

// Stary test w tym samym projekcie:
// import org.junit.Test;  // JUnit 4
// class StaryTest {
//     @Test public void test() {
//         // Uruchomi go VintageTestEngine
//     }
// }

Materiały

Jakie są kluczowe różnice między JUnit 4 a JUnit 5?

Odpowiedź w 30 sekund: JUnit 5 wprowadza modułową architekturę (Platform + Jupiter + Vintage), wymaga Javy 8+, zmienia pakiety na org.junit.jupiter.*, zastępuje @Runner i @Rule jednolitym modelem rozszerzeń (@ExtendWith), oferuje bogatsze asercje (lambdy, assertAll, assertThrows) oraz natywne wsparcie dla testów parametryzowanych, dynamicznych i zagnieżdżonych.

Odpowiedź w 2 minuty: JUnit 5 to całkowite przeprojektowanie frameworka, dlatego różnic jest wiele. Najważniejsze obejmują zmianę pakietów (z org.junit.* na org.junit.jupiter.api.*), wymaganie Javy 8 lub nowszej, a także zmiany nazw adnotacji - @Before stało się @BeforeEach, @BeforeClass przekształciło się w @BeforeAll, a @Ignore w @Disabled.

Najbardziej fundamentalna zmiana to porzucenie modelu @RunWith i @Rule na rzecz jednolitego mechanizmu rozszerzeń (@ExtendWith). W JUnit 4 nie można było używać kilku runnerów jednocześnie, co prowadziło do konfliktów (np. Spring Runner vs Mockito Runner). W JUnit 5 można dowolnie kompować rozszerzenia.

Asercje w JUnit 5 wykorzystują lambdy - wprowadzono assertThrows dla wyjątków (zamiast expected w @Test), assertTimeout dla limitów czasu (zamiast timeout w @Test), assertAll dla grupowania asercji oraz assertDoesNotThrow. Testy parametryzowane stały się częścią głównego API (@ParameterizedTest), a nie osobnego runnera. Doszły też zupełnie nowe koncepcje: @DisplayName, @Nested, @Tag, @TestFactory, @RepeatedTest.

Aspekt JUnit 4 JUnit 5 (Jupiter)
Pakiet główny org.junit org.junit.jupiter.api
Wymagana Java Java 5+ Java 8+
Architektura Monolit Modułowa (Platform + Engines)
Adnotacja testu @Test @Test (inny pakiet)
Setup metody @Before, @After @BeforeEach, @AfterEach
Setup klasy @BeforeClass, @AfterClass @BeforeAll, @AfterAll
Wyłączanie testu @Ignore @Disabled
Rozszerzenia @RunWith + @Rule (sztywne) @ExtendWith (kompozycyjne)
Wyjątki @Test(expected = Ex.class) assertThrows(Ex.class, () -> ...)
Timeout @Test(timeout = 1000) assertTimeout(Duration.ofMillis(1000), ...)
Asercje grupowe brak assertAll(...)
Testy parametryzowane @RunWith(Parameterized.class) @ParameterizedTest natywnie
Czytelna nazwa brak @DisplayName("...")
Testy zagnieżdżone brak @Nested
Tagowanie @Category(...) @Tag("...")

Przykład kodu:

// ===== JUnit 4 (stary styl) =====
import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.*;

@RunWith(Parameterized.class)  // Tylko jeden runner naraz!
public class StaryTest {

    @Before
    public void setup() { /* przygotowanie */ }

    @Test(expected = IllegalArgumentException.class,
          timeout = 1000)
    public void testWyjatek() {
        wykonaj(null);
    }
}

// ===== JUnit 5 (Jupiter - nowy styl) =====
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;

@DisplayName("Testy nowego stylu")
class NowyTest {

    @BeforeEach
    void setup() { /* przygotowanie przed każdym testem */ }

    @Test
    @DisplayName("Sprawdzenie wyjątku z limitem czasu")
    void testWyjatek() {
        // Wyjątek jako lambda + timeout jako asercja
        assertTimeout(Duration.ofSeconds(1), () -> {
            IllegalArgumentException ex = assertThrows(
                IllegalArgumentException.class,
                () -> wykonaj(null)
            );
            assertEquals("argument null", ex.getMessage());
        });
    }

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 5, 8})
    void testParametryzowany(int wartosc) {
        // Test uruchomi się 5 razy z różnymi wartościami
        assertTrue(wartosc > 0);
    }

    private void wykonaj(Object o) {
        if (o == null) throw new IllegalArgumentException("argument null");
    }
}

Materiały

Jak dodać JUnit 5 do projektu Maven i Gradle?

Odpowiedź w 30 sekund: W Maven dodajemy zależność junit-jupiter (lub junit-jupiter-api + junit-jupiter-engine) w scope test oraz konfigurujemy maven-surefire-plugin w wersji 2.22.0+. W Gradle używamy testImplementation 'org.junit.jupiter:junit-jupiter' oraz włączamy useJUnitPlatform() w bloku test. Od Gradle 4.6 i Surefire 2.22.0 wsparcie dla JUnit Platform jest natywne.

Odpowiedź w 2 minuty: Maven - od wersji Surefire 2.22.0 (i Failsafe) wsparcie dla JUnit Platform jest wbudowane, więc wystarczy dodać zależność na artefakt junit-jupiter z grupy org.junit.jupiter. Artefakt junit-jupiter to agregator, który ściąga junit-jupiter-api, junit-jupiter-engine oraz junit-jupiter-params. Wcześniej w starszych wersjach Surefire potrzebny był dodatkowy provider, dziś nie jest to konieczne. Dla projektów testujących stary kod JUnit 4 dodajemy też junit-vintage-engine.

Gradle - od wersji 4.6 Gradle ma natywne wsparcie dla JUnit Platform. Włączamy je wywołując useJUnitPlatform() w konfiguracji zadania test. Zależność org.junit.jupiter:junit-jupiter dodajemy do konfiguracji testImplementation. Bez wywołania useJUnitPlatform() Gradle będzie próbował uruchomić testy starym mechanizmem JUnit 4 i ich nie znajdzie.

W obu narzędziach warto zarządzać wersją centralnie - w Maven przez <dependencyManagement> z BOM-em junit-bom, w Gradle przez platform("org.junit:junit-bom:5.x.x"). BOM zapewnia spójność wersji wszystkich artefaktów JUnita (Jupiter, Platform, Vintage).

Przykład kodu:

<!-- ===== pom.xml (Maven) - minimalna konfiguracja ===== -->
<project>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <junit.version>5.10.2</junit.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- BOM zapewnia spójne wersje wszystkich modułów JUnit -->
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>${junit.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Agregator: API + engine + params -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Opcjonalnie: wsparcie dla starych testów JUnit 4 -->
        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Surefire 2.22.0+ wspiera JUnit Platform natywnie -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>
// ===== build.gradle (Gradle) - minimalna konfiguracja =====
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    // BOM dla spójnych wersji modułów JUnit
    testImplementation platform('org.junit:junit-bom:5.10.2')

    // Agregator Jupiter (API + engine + params)
    testImplementation 'org.junit.jupiter:junit-jupiter'

    // Opcjonalnie: silnik Vintage dla starych testów JUnit 4
    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
}

test {
    // KLUCZOWE: bez tego Gradle nie odkryje testów Jupiter!
    useJUnitPlatform()

    // Wyświetlanie wyników testów w konsoli
    testLogging {
        events 'passed', 'failed', 'skipped'
    }
}
// ===== build.gradle.kts (Gradle Kotlin DSL) =====
plugins {
    java
}

dependencies {
    testImplementation(platform("org.junit:junit-bom:5.10.2"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}

tasks.test {
    useJUnitPlatform()  // Włącz JUnit Platform
}

Materiały

Jak skonfigurować JUnit Platform do uruchamiania testów (junit-platform.properties)?

Odpowiedź w 30 sekund: Plik junit-platform.properties umieszczany w src/test/resources/ pozwala konfigurować zachowanie JUnit Platform deklaratywnie. Pozwala on m.in. włączyć równoległe wykonywanie testów (junit.jupiter.execution.parallel.enabled=true), zmienić domyślny cykl życia (lifecycle.per_class), ustawić strategię nazewnictwa testów, włączyć automatyczne wykrywanie rozszerzeń oraz definiować filtry tagów.

Odpowiedź w 2 minuty: JUnit Platform odczytuje konfigurację z trzech źródeł (w kolejności priorytetu): parametrów Launcher, właściwości systemowych (-D) i pliku junit-platform.properties znajdującego się w classpath (zwykle src/test/resources/). Plik ten to zwykły plik properties zawierający pary klucz-wartość, które wpływają na sposób odkrywania, uruchamiania i raportowania testów.

Najważniejsze opcje konfiguracyjne dotyczą równoległego wykonywania testów - włączamy je przez junit.jupiter.execution.parallel.enabled=true i kontrolujemy ziarnistość przez parallel.mode.default (same_thread lub concurrent) oraz parallel.mode.classes.default. Strategia tworzenia puli wątków konfigurowana jest przez parallel.config.strategy (dynamic, fixed lub custom).

Drugim ważnym zestawem opcji jest cykl życia testów - domyślnie JUnit 5 tworzy nową instancję klasy testowej dla każdej metody (per_method), ale można to zmienić na per_class przez junit.jupiter.testinstance.lifecycle.default=per_class. Można też automatycznie wykrywać rozszerzenia przez junit.jupiter.extensions.autodetection.enabled=true (mechanizm ServiceLoader). Inne przydatne opcje to ustawienie domyślnego generatora nazw (displayname.generator.default) i włączenie deterministycznej kolejności testów (testmethod.order.default).

Konfiguracja deklaratywna w pliku properties ma przewagę nad konfiguracją programistyczną - jest wersjonowana z kodem, nie wymaga rekompilacji i działa z każdym narzędziem uruchamiającym (Maven, Gradle, IDE).

Przykład kodu:

# ===== src/test/resources/junit-platform.properties =====

# --- Cykl życia testów ---
# Domyślnie: per_method (nowa instancja na metodę)
# per_class: jedna instancja na klasę (przydatne dla @BeforeAll niestatycznych)
junit.jupiter.testinstance.lifecycle.default = per_class

# --- Równoległe wykonywanie testów ---
# Włącz równoległe wykonywanie
junit.jupiter.execution.parallel.enabled = true

# Domyślny tryb dla metod: concurrent (równolegle) lub same_thread (sekwencyjnie)
junit.jupiter.execution.parallel.mode.default = concurrent

# Tryb dla klas testowych
junit.jupiter.execution.parallel.mode.classes.default = concurrent

# Strategia puli wątków: dynamic | fixed | custom
junit.jupiter.execution.parallel.config.strategy = dynamic

# Dla strategii fixed - liczba wątków
# junit.jupiter.execution.parallel.config.fixed.parallelism = 4

# --- Generator nazw testów ---
# Zamienia camelCase na spacje w nazwach metod
junit.jupiter.displayname.generator.default = \
    org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

# --- Kolejność testów ---
# Domyślna kolejność wykonywania metod (Random, Alphanumeric, MethodName)
junit.jupiter.testmethod.order.default = \
    org.junit.jupiter.api.MethodOrderer$OrderAnnotation

# --- Automatyczne wykrywanie rozszerzeń ---
# Włącza ServiceLoader dla rozszerzeń (META-INF/services)
junit.jupiter.extensions.autodetection.enabled = true

# --- Wartość domyślna dla testów warunkowych ---
junit.jupiter.conditions.deactivate = *DisabledOnOs

# --- Wykonanie testów po nieudanym założeniu (assumption) ---
# junit.jupiter.execution.timeout.default = 5s
# junit.jupiter.execution.timeout.testable.method.default = 10s
// Przykład testu wykorzystującego powyższą konfigurację
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

// Dzięki lifecycle=per_class metoda @BeforeAll może być NIEstatyczna
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(ExecutionMode.CONCURRENT)  // Równolegle z konfiguracji
class KonfiguracjaTest {

    private ZasobyTestowe zasoby;

    @BeforeAll
    void przygotujZasoby() {
        // Niestatyczna metoda - możliwa dzięki per_class
        zasoby = new ZasobyTestowe();
    }

    @Test
    @Order(1)  // Kontrola kolejności (OrderAnnotation)
    void test_pierwszy_w_kolejnosci() {
        // Nazwa wyświetli się jako: "test pierwszy w kolejnosci"
        // (ReplaceUnderscores zamienia podkreślenia na spacje)
        Assertions.assertNotNull(zasoby);
    }

    @Test
    @Order(2)
    void test_drugi_w_kolejnosci() {
        Assertions.assertTrue(zasoby.isGotowy());
    }

    static class ZasobyTestowe {
        boolean isGotowy() { return true; }
    }
}

Materiały

Anotacje podstawowe

Czym różnią się @Test w JUnit 4 i JUnit 5 (zmieniony pakiet, brak parametrów)?

Odpowiedź w 30 sekund: W JUnit 4 adnotacja @Test pochodzi z pakietu org.junit i akceptuje parametry takie jak expected i timeout. W JUnit 5 @Test pochodzi z org.junit.jupiter.api i nie ma żadnych parametrów — sprawdzanie wyjątków przeniesiono do assertThrows, a limit czasu do assertTimeout lub adnotacji @Timeout. Metody testowe nie muszą już być public.

Odpowiedź w 2 minuty: Najbardziej widoczna różnica to zmiana pakietu. JUnit 4 używa starego org.junit.Test, podczas gdy JUnit 5 (Jupiter) wprowadza nowy org.junit.jupiter.api.Test. Import to pierwszy sygnał, że pracujemy z nowszą wersją frameworka.

W JUnit 4 adnotacja przyjmowała parametry: @Test(expected = IllegalArgumentException.class) oraz @Test(timeout = 1000). W JUnit 5 te możliwości zostały rozdzielone na osobne mechanizmy: oczekiwane wyjątki testujemy przez assertThrows, a limity czasowe przez @Timeout lub assertTimeout. Daje to czytelniejsze testy i lepsze komunikaty błędów (np. assertThrows zwraca wyjątek, który można dalej weryfikować).

Druga znacząca zmiana to brak wymogu publicznych klas i metod testowych. W JUnit 5 testy mogą być package-private, co lepiej pasuje do zasad enkapsulacji. Cykl życia testów również się zmienił: @Before → @BeforeEach, @After → @AfterEach, @BeforeClass → @BeforeAll, @AfterClass → @AfterAll. JUnit 5 oferuje też nowe funkcje jak @DisplayName, @Nested, @ParameterizedTest oraz rozszerzenia przez @ExtendWith.

%%{init: {'theme':'base'}}%%
flowchart LR
    subgraph JUnit4["JUnit 4"]
        A1["org.junit.Test"]
        A2["@Test(expected = Ex.class)"]
        A3["@Test(timeout = 1000)"]
        A4["public class + public methods"]
    end
    subgraph JUnit5["JUnit 5 (Jupiter)"]
        B1["org.junit.jupiter.api.Test"]
        B2["assertThrows(Ex.class, ...)"]
        B3["@Timeout / assertTimeout"]
        B4["package-private OK"]
    end
    A1 -->|migracja| B1
    A2 -->|migracja| B2
    A3 -->|migracja| B3
    A4 -->|migracja| B4
Cecha JUnit 4 (@Test) JUnit 5 (@Test)
Pakiet org.junit.Test org.junit.jupiter.api.Test
Parametr expected Tak (expected = Ex.class) Brak — użyj assertThrows
Parametr timeout Tak (timeout = 1000) Brak — użyj @Timeout / assertTimeout
Widoczność metody public wymagana Dowolna (poza private)
Widoczność klasy public wymagana Dowolna (poza private)
Lifecycle @Before, @After @BeforeEach, @AfterEach
Runner / Extension @RunWith @ExtendWith

Przykład kodu:

// JUnit 4 - stary styl
import org.junit.Test;

public class KalkulatorTestJUnit4 {

    // Parametry expected i timeout bezpośrednio w adnotacji
    @Test(expected = ArithmeticException.class, timeout = 1000)
    public void powinienRzucicWyjatekPrzyDzieleniuPrzezZero() {
        new Kalkulator().dziel(10, 0);
    }
}

// JUnit 5 - nowy styl
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;

class KalkulatorTestJUnit5 {  // klasa nie musi być public

    @Test
    @Timeout(value = 1, unit = TimeUnit.SECONDS)
    void powinienRzucicWyjatekPrzyDzieleniuPrzezZero() {  // metoda nie musi być public
        // Sprawdzamy wyjątek przez assertThrows zamiast parametru expected
        ArithmeticException ex = assertThrows(
            ArithmeticException.class,
            () -> new Kalkulator().dziel(10, 0)
        );
        // Możemy dodatkowo zweryfikować komunikat wyjątku
        assertEquals("/ by zero", ex.getMessage());
    }
}

Materiały

Do czego służy @DisplayName i jakie są dobre praktyki nazewnictwa testów?

Odpowiedź w 30 sekund: @DisplayName pozwala nadać klasie testowej lub metodzie czytelną nazwę wyświetlaną w raportach IDE i CI zamiast nazwy metody Javy. Dzięki temu testy mogą używać polskich znaków, spacji, emoji oraz pełnych zdań opisujących oczekiwane zachowanie. Dobre praktyki to opisywanie zachowania ("Powinien..."), a nie implementacji.

Odpowiedź w 2 minuty: Standardowe nazwy metod w Javie są ograniczone — nie mogą zawierać spacji, znaków specjalnych ani polskich liter z diakrytykami. @DisplayName rozwiązuje ten problem: nazwa z adnotacji pojawia się w wynikach testów (IntelliJ, Eclipse, raporty Surefire, Allure), zachowując pełną informację dla osoby przeglądającej rezultaty.

Można jej używać na poziomie klasy (@DisplayName("Kalkulator")) i na poziomie metody. Często łączy się to ze stylem BDD: "Should... when..." lub po polsku "Powinien... gdy...". Dzięki temu raport testów staje się czytelną specyfikacją zachowania systemu.

Dobre praktyki nazewnictwa testów:

  • Opisuj zachowanie, nie implementację — "Powinien zwrócić listę użytkowników gdy baza zawiera dane" zamiast "testGetUsersFromDb".
  • Używaj formuły Given/When/Then lub Powinien/Gdy — czytelnie wskazuje warunki i oczekiwany rezultat.
  • Jeden test = jedno zachowanie — nazwa powinna odzwierciedlać dokładnie jedną zweryfikowaną własność.
  • Grupuj @Nested — pozwala tworzyć hierarchiczne nazwy: klasa "Kalkulator" → zagnieżdżona klasa "Gdy dzielimy" → test "Powinien rzucić wyjątek dla 0".
  • Nie powtarzaj nazwy klasy w nazwie testu — kontekst i tak jest widoczny.

@DisplayNameGeneration pozwala wygenerować nazwy automatycznie z nazw metod (np. zamieniając podkreślenia na spacje), co jest alternatywą dla ręcznego dodawania adnotacji.

Przykład kodu:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Kalkulator - operacje arytmetyczne")
class KalkulatorTest {

    private final Kalkulator kalkulator = new Kalkulator();

    @Test
    @DisplayName("Powinien zwracać 5 gdy dodajemy 2+3")
    void dodawanieDwochLiczb() {
        // Given - dwie liczby do dodania
        int a = 2;
        int b = 3;

        // When - wykonujemy dodawanie
        int wynik = kalkulator.dodaj(a, b);

        // Then - oczekujemy poprawnego wyniku
        assertEquals(5, wynik);
    }

    @Test
    @DisplayName("Powinien rzucić ArithmeticException gdy dzielimy przez zero")
    void dzielenieprzezZero() {
        assertThrows(ArithmeticException.class, () -> kalkulator.dziel(10, 0));
    }

    @Nested
    @DisplayName("Gdy liczby są ujemne")
    class LiczbyUjemne {

        @Test
        @DisplayName("Powinien zwrócić wartość ujemną dla -2 + -3")
        void dodawanieDwochLiczbUjemnych() {
            assertEquals(-5, kalkulator.dodaj(-2, -3));
        }
    }
}

// Alternatywa - automatyczne generowanie nazw z metod
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class AutoNazewnictwoTest {

    @Test
    void Powinien_zwrocic_pusta_liste_gdy_brak_uzytkownikow() {
        // Nazwa wyświetlana: "Powinien zwrocic pusta liste gdy brak uzytkownikow"
    }
}

Materiały

Jak działa @Disabled i kiedy warto wyłączyć test zamiast go usuwać?

Odpowiedź w 30 sekund: @Disabled wyłącza test lub całą klasę testową — nie jest wykonywany, ale pozostaje w kodzie i raportach jako pominięty. Warto z niej korzystać, gdy test jest tymczasowo niestabilny, czeka na fix błędu w innej usłudze albo gdy chcemy zachować widoczny ślad zadania do zrobienia z numerem ticketu. JUnit 5 oferuje też warianty warunkowe: @DisabledOnOs, @DisabledIfEnvironmentVariable.

Odpowiedź w 2 minuty: @Disabled to odpowiednik @Ignore z JUnit 4. Można ją umieścić na metodzie testowej (wyłącza pojedynczy test) lub na klasie (wyłącza wszystkie testy w klasie). Pominięty test jest widoczny w raporcie z odpowiednim oznaczeniem — to ważne, bo nie traci się informacji, że test istnieje i czeka na decyzję.

Adnotacja przyjmuje opcjonalny opis — najlepiej w nim wskazać powód i sposób przywrócenia testu. Dobre praktyki to dodać numer ticketu (FLIP-1234), opis blockera ("API zewnętrzne niedostępne w sandbox"), lub TODO z datą. Wyłączony test bez komentarza po pół roku staje się śmieciem w kodzie.

Kiedy warto wyłączać zamiast usuwać:

  • Czekamy na fix błędu — test demaskuje znany bug, ale jeszcze nie jest naprawiony.
  • Test jest flaky — chwilowo wyłączamy do czasu zdiagnozowania źródła niestabilności.
  • Brakuje danych testowych / infrastruktury — np. zewnętrzny system stub jeszcze niegotowy.
  • Test pisany w stylu TDD wyprzedzająco — opisany scenariusz, ale produkt go jeszcze nie obsługuje.

JUnit 5 oferuje też warunkowe wyłączanie, znacznie potężniejsze od statycznego @Disabled:

  • @DisabledOnOs(OS.WINDOWS) — wyłącza na konkretnym systemie operacyjnym.
  • @DisabledOnJre(JRE.JAVA_8) — wyłącza na konkretnej wersji JRE.
  • @DisabledIfEnvironmentVariable(named = "CI", matches = "true") — wyłącza w pipeline CI.
  • @DisabledIfSystemProperty(named = "env", matches = "prod") — wyłącza dla danej property.

Analogiczne wersje @EnabledOn... wykonują test tylko w określonych warunkach.

Przykład kodu:

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.*;

class TestyWylaczone {

    @Test
    @Disabled("TODO: FLIP-1234 - naprawić po wdrożeniu nowego API płatności")
    void powinienObliczacZnizkePremiumDlaKlientaVIP() {
        // Test czeka na nową logikę zniżek
        fail("Nieaktywny do czasu zmian w PaymentService");
    }

    @Test
    @Disabled("Blocker: zewnętrzne API sandbox PayU niedostępne (zgłoszone 2026-04-15)")
    void powinienPrzeprocesowacPlatnoscPayU() {
        // Czekamy na odpowiedź supportu PayU
    }

    // Wyłączenie warunkowe - tylko poza Windows
    @Test
    @DisabledOnOs(OS.WINDOWS)
    void powinienUtworzycSymlinkNaUnix() {
        // Symlinki działają inaczej na Windows
    }

    // Wyłączenie w środowisku CI
    @Test
    @DisabledIfEnvironmentVariable(named = "CI", matches = "true")
    void testInterakcyjnyZUI() {
        // Test wymaga prawdziwego ekranu - nie uruchamiamy na CI
    }

    // Wyłączenie na produkcji
    @Test
    @DisabledIfSystemProperty(named = "spring.profiles.active", matches = "prod")
    void testGenerujacyDuzeIlosciDanych() {
        // Test obciążeniowy - tylko na środowisku dev/test
    }
}

// Cała klasa wyłączona
@Disabled("Migracja do nowego frameworka - patrz FLIP-5678")
class StareTestyIntegracyjneTest {

    @Test
    void test1() { /* ... */ }

    @Test
    void test2() { /* ... */ }
}

Materiały

Czym są @Tag i jak filtrować testy po tagach w Maven/Gradle?

Odpowiedź w 30 sekund: @Tag pozwala oznaczać testy etykietami (np. "slow", "integration", "smoke") i selektywnie uruchamiać podzbiory podczas budowania projektu. W Mavenie konfiguruje się je w maven-surefire-plugin przez <groups> i <excludedGroups>, w Gradle przez useJUnitPlatform { includeTags / excludeTags }. Dzięki temu możemy mieć szybkie testy w PR i wolne testy E2E w pipeline nightly.

Odpowiedź w 2 minuty: @Tag to mechanizm grupowania testów w JUnit 5. Adnotację można umieścić na klasie lub metodzie i przyjmuje jeden string (lub można użyć wielu adnotacji @Tag). Tagi są dowolnymi etykietami zdefiniowanymi przez zespół — typowe konwencje to: unit, integration, e2e, slow, fast, smoke, regression, db, external.

Filtrowanie odbywa się na poziomie launchera testów, więc działa zarówno w IDE, Maven, jak i Gradle. W Maven maven-surefire-plugin (testy jednostkowe) i maven-failsafe-plugin (testy integracyjne) przyjmują:

  • <groups>tag1 | tag2</groups> — uruchom testy z tymi tagami.
  • <excludedGroups>slow</excludedGroups> — pomiń testy z tym tagiem.

Można też używać wyrażeń logicznych: unit & !slow, (integration | e2e) & !flaky.

W Gradle konfiguracja jest podobna i odbywa się w bloku test:

test {
    useJUnitPlatform {
        includeTags 'fast'
        excludeTags 'slow', 'flaky'
    }
}

Najlepsze praktyki:

  • Twórz meta-adnotacje — @IntegrationTest jako kombinacja @Tag("integration") i ewentualnie innych adnotacji, by uniknąć powtarzania literałów.
  • Definiuj profile Maven — mvn test -P fast (tylko szybkie), mvn verify -P integration (testy integracyjne).
  • Tagi powinny być nieliczne i znaczące — 5-7 typów wystarczy w 99% projektów.
  • Dokumentuj konwencję — co znaczy slow (np. >1s), kiedy używać e2e itd.
flowchart TD
    A["Uruchomienie testów"] --> B{"Środowisko"}
    B -->|Lokalnie / PR| C["mvn test -P fast"]
    B -->|Nightly CI| D["mvn verify -P all"]
    B -->|Smoke check| E["mvn test -P smoke"]
    C --> F["maven-surefire-plugin"]
    D --> F
    E --> F
    F --> G["&lt;groups&gt; fast<br/>&lt;excludedGroups&gt; slow,e2e"]
    G --> H["JUnit Platform<br/>filtruje @Tag"]
    H --> I["Wykonanie tylko<br/>pasujących testów"]

Przykład kodu:

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Tags;
import org.junit.jupiter.api.Test;

@Tag("integration")
@Tag("db")
class RepositoryIntegrationTest {

    @Test
    void powinienZapisacUzytkownikaDoBazy() {
        // Test integracyjny z bazą danych
    }
}

class KalkulatorTest {

    @Test
    @Tag("fast")
    @Tag("unit")
    void powinienDodawacLiczby() {
        // Szybki test jednostkowy
    }

    @Test
    @Tag("slow")
    void powinienObliczacFibonacciegoDlaDuzychLiczb() {
        // Wolny test - wyłączony w pipeline PR
    }
}

// Meta-adnotacja - własna adnotacja kombinująca @Tag
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Tag("integration")
@Tag("db")
public @interface IntegrationTest {
}

// Użycie meta-adnotacji
@IntegrationTest
class UserServiceIntegrationTest {
    // Wszystkie testy w klasie mają tagi "integration" i "db"
}

Konfiguracja maven-surefire-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <!-- Uruchom tylko testy z tagiem "fast" lub "unit" -->
        <groups>fast | unit</groups>
        <!-- Pomiń testy oznaczone jako wolne lub flaky -->
        <excludedGroups>slow | flaky</excludedGroups>
    </configuration>
</plugin>

<!-- Testy integracyjne w osobnym pluginie -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <groups>integration | e2e</groups>
    </configuration>
</plugin>

Konfiguracja Gradle:

test {
    useJUnitPlatform {
        includeTags 'fast', 'unit'
        excludeTags 'slow', 'flaky'
    }
}

// Osobny task dla testów integracyjnych
task integrationTest(type: Test) {
    useJUnitPlatform {
        includeTags 'integration', 'e2e'
    }
}

Materiały

Jak działa @Timeout w JUnit 5 i czym różni się od assertTimeout?

Odpowiedź w 30 sekund: @Timeout to adnotacja deklaratywna nałożona na klasę lub metodę — przerywa test, gdy przekroczy zadany czas. assertTimeout to programowe wywołanie wewnątrz metody testowej, mierzące czas wybranego fragmentu kodu. @Timeout przerywa wykonanie po przekroczeniu limitu (z domyślnym THREAD mode), a assertTimeout (bez _preemptively) czeka aż kod skończy działanie i dopiero potem zgłasza błąd.

Odpowiedź w 2 minuty: JUnit 5 oferuje trzy mechanizmy ograniczania czasu wykonania testów: adnotację @Timeout, assertTimeout oraz assertTimeoutPreemptively.

@Timeout to podejście deklaratywne. Można ją nałożyć na metodę testową, metodę cyklu życia (@BeforeEach, @AfterEach) lub całą klasę — wtedy działa na wszystkie testy w niej zawarte. Akceptuje wartość i jednostkę: @Timeout(value = 500, unit = TimeUnit.MILLISECONDS). Domyślnie używa trybu THREAD, gdzie test wykonuje się w osobnym wątku i jest przerywany po przekroczeniu limitu. Można też globalnie ustawić timeouty w junit-platform.properties (np. junit.jupiter.execution.timeout.default = 30 s).

assertTimeout(Duration, Executable) to programowe podejście — mierzy czas wykonania bloku kodu i zgłasza błąd dopiero PO jego zakończeniu. Oznacza to, że jeśli kod się "zawiesi", assertTimeout też się zawiesi (nie przerywa wykonania). Plus: nie potrzebuje osobnego wątku, więc działa z ThreadLocal, transakcjami Spring i podobnymi mechanizmami zależnymi od wątku.

assertTimeoutPreemptively(Duration, Executable) przerywa kod od razu po przekroczeniu czasu (jak @Timeout), uruchamiając Executable w osobnym wątku. Uwaga: nie zachowuje stanu ThreadLocal — może powodować problemy ze Spring Transaction Manager czy SecurityContext.

Kiedy używać której:

  • @Timeout — gdy chcesz prostego, deklaratywnego limitu dla całej klasy/metody.
  • assertTimeout — gdy mierzysz tylko fragment testu i nie boisz się, że kod się zawiesi (lub gdy potrzebujesz wątku z ThreadLocal).
  • assertTimeoutPreemptively — gdy chcesz "twardo" przerywać i nie używasz ThreadLocal-zależnych mechanizmów.
Cecha @Timeout assertTimeout assertTimeoutPreemptively
Typ Adnotacja deklaratywna Wywołanie programowe Wywołanie programowe
Miejsce Klasa / metoda Wewnątrz metody Wewnątrz metody
Przerywa po przekroczeniu Tak (domyślnie THREAD) Nie - czeka na zakończenie Tak
Osobny wątek Tak Nie Tak
Działa z ThreadLocal Nie (w domyślnym trybie) Tak Nie
Globalna konfiguracja Tak (properties) Nie Nie
Zwraca wynik Nie Tak (T) Tak (T)

Przykład kodu:

import org.junit.jupiter.api.*;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;

// @Timeout na klasie - dotyczy wszystkich testów
@Timeout(value = 5, unit = TimeUnit.SECONDS)
class UslugaTest {

    private final WolnaUsluga usluga = new WolnaUsluga();

    // Wykorzystuje timeout z klasy (5 sekund)
    @Test
    void powinienPobracDaneSzybkoDomyslny() {
        usluga.pobierzDane();
    }

    // Override timeout dla konkretnego testu
    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    void powinienPobracDaneZCacheBardzoSzybko() {
        // Maksymalnie 500ms na cache hit
        usluga.pobierzZCache();
    }

    // assertTimeout - mierzymy tylko wybraną część
    @Test
    void powinienObliczycSumeWCzasie() {
        // Przygotowanie danych nie liczy się do limitu
        List<Integer> liczby = przygotujDane();

        // Mierzymy tylko obliczanie sumy
        // UWAGA: czeka na zakończenie - nie przerywa
        int suma = assertTimeout(
            Duration.ofMillis(100),
            () -> liczby.stream().mapToInt(Integer::intValue).sum()
        );

        assertEquals(5050, suma);
    }

    // assertTimeoutPreemptively - twardo przerywa po czasie
    @Test
    void powinienPrzerwacGdyMetodaSieZawiesza() {
        assertTimeoutPreemptively(
            Duration.ofMillis(200),
            () -> {
                // Ten kod jest uruchamiany w osobnym wątku
                // i zostanie przerwany po 200ms
                while (true) {
                    Thread.sleep(10);
                }
            }
        );
    }
}

Globalna konfiguracja w src/test/resources/junit-platform.properties:

# Domyślny timeout dla wszystkich testów
junit.jupiter.execution.timeout.default = 30 s

# Timeout tylko dla metod cyklu życia
junit.jupiter.execution.timeout.lifecycle.method.default = 10 s

# Wyłączenie timeoutów w trybie debug
junit.jupiter.execution.timeout.mode = disabled_on_debug

Materiały

Asercje

Jakie są podstawowe metody klasy Assertions (assertEquals, assertTrue, assertNotNull)?

Odpowiedź w 30 sekund: Klasa org.junit.jupiter.api.Assertions to statyczna fasada z metodami sprawdzającymi oczekiwania w teście. Najczęściej używane to assertEquals (porównanie wartości), assertTrue/assertFalse (warunek logiczny), assertNotNull/assertNull (referencja). Każda metoda przyjmuje opcjonalny komunikat błędu jako ostatni parametr.

Odpowiedź w 2 minuty: W JUnit 5 wszystkie asercje znajdują się w klasie Assertions z pakietu org.junit.jupiter.api. Importuje się je statycznie (import static org.junit.jupiter.api.Assertions.*), aby kod testu był zwięzły. Asercja, która się nie powiedzie, rzuca AssertionFailedError i kończy bieżący test jako "failed".

Najważniejsze grupy metod to: porównania (assertEquals, assertNotEquals, assertArrayEquals, assertIterableEquals, assertLinesMatch), warunki logiczne (assertTrue, assertFalse), sprawdzanie referencji (assertNull, assertNotNull, assertSame, assertNotSame) oraz wymuszanie błędu (fail). Pamiętaj o kolejności argumentów: zawsze expected przed actual — odwrócenie tej kolejności daje mylące komunikaty błędów.

Każda metoda ma trzy warianty: bez komunikatu, z komunikatem typu String oraz z Supplier<String> (komunikat budowany leniwie, dopiero gdy asercja faktycznie zawiedzie — przydatne, gdy budowa komunikatu jest kosztowna). Dla typów zmiennoprzecinkowych używaj wariantu z delta (np. assertEquals(0.1, result, 0.0001)), bo double i float nie nadają się do porównań dokładnych.

Przykład kodu:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    @Test
    void powinienDodawaeLiczby() {
        Calculator calc = new Calculator();

        int wynik = calc.dodaj(2, 3);

        // Porownanie wartosci: najpierw expected, potem actual
        assertEquals(5, wynik, "Dodawanie 2+3 powinno dac 5");

        // Warunek logiczny
        assertTrue(wynik > 0, "Wynik powinien byc dodatni");
        assertFalse(wynik == 0, "Wynik nie powinien byc zerem");

        // Sprawdzanie referencji
        Object obj = calc.pobierzKontekst();
        assertNotNull(obj, "Kontekst nie moze byc null");

        // Porownanie liczb zmiennoprzecinkowych z tolerancja
        assertEquals(0.3, calc.dodajDouble(0.1, 0.2), 0.0001,
            "Suma 0.1+0.2 powinna byc bliska 0.3");

        // Leniwy komunikat - budowany dopiero przy bledzie
        assertEquals("oczekiwane", calc.pobierzTekst(),
            () -> "Drogi log: " + calc.pobierzPelnyStan());
    }

    @Test
    void powinienPorownaeTablice() {
        int[] oczekiwane = {1, 2, 3};
        int[] aktualne = {1, 2, 3};
        assertArrayEquals(oczekiwane, aktualne, "Tablice powinny byc rowne");
    }
}

Materiały

Czym jest assertAll i kiedy warto go używać zamiast sekwencji pojedynczych asercji?

Odpowiedź w 30 sekund: assertAll to "grouped assertions" — wykonuje wszystkie przekazane asercje, nawet jeśli wcześniejsze zawiodą, a na końcu raportuje zbiorczo wszystkie błędy w jednym MultipleFailuresError. Domyślnie pojedyncze asercje są fail-fast (pierwsza porażka kończy test), więc assertAll używamy, gdy chcemy zobaczyć wszystkie problemy naraz — szczególnie przy weryfikacji wielu pól tego samego obiektu.

Odpowiedź w 2 minuty: Standardowy przepływ JUnit jest fail-fast: gdy pierwsza asercja w teście zawiedzie, JVM rzuca AssertionFailedError i pozostałe linie się nie wykonują. To dobrze dla niezależnych asercji, ale problematyczne, gdy chcemy zweryfikować kilka właściwości jednego rezultatu — naprawiamy pierwszy błąd, uruchamiamy test, widzimy drugi, naprawiamy, uruchamiamy znowu... iteracja po iteracji.

Assertions.assertAll(Executable...) rozwiązuje ten problem: przyjmuje wiele lambd, wykonuje każdą i kolekcjonuje wyjątki. Jeśli choć jedna zawiedzie, rzuca MultipleFailuresError z listą wszystkich błędów. Daje to pełny obraz po jednym uruchomieniu testu.

Kiedy używać: weryfikacja wielu pól DTO/encji, sprawdzanie powiązanych właściwości jednego stanu (np. wszystkie pola odpowiedzi REST), testy gdzie błędy są powiązane logicznie. Kiedy NIE używać: gdy asercje są od siebie zależne (np. najpierw assertNotNull(list), potem assertEquals(3, list.size()) — sprawdzenie rozmiaru null-a wywoła NPE, a assertAll nie chroni przed wyjątkami runtime'owymi w sensowny sposób — lepsze są wtedy zagnieżdżone assertAll lub osobne asercje).

flowchart LR
    subgraph Fail-Fast["Sekwencja asercji (fail-fast)"]
        A1[assertEquals - OK] --> A2[assertEquals - FAIL]
        A2 -.X.-> A3[assertEquals - nie wykonana]
    end
    subgraph AssertAll["assertAll (zbiorczy raport)"]
        B1[assertEquals - OK] --> B2[assertEquals - FAIL]
        B2 --> B3[assertEquals - OK]
        B3 --> B4[MultipleFailuresError z 1 bledem]
    end

Przykład kodu:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UzytkownikTest {

    @Test
    void powinienZwroceUzytkownikaZPelnymiDanymi() {
        Uzytkownik user = serwis.znajdz(1L);

        // BEZ assertAll - fail-fast, zobaczymy tylko pierwszy blad
        // assertEquals("Jan", user.getImie());     // FAIL -> stop
        // assertEquals("Kowalski", user.getNazwisko()); // nie wykona sie
        // assertEquals(30, user.getWiek());             // nie wykona sie

        // Z assertAll - wszystkie asercje sa wykonane, raport zbiorczy
        assertAll("Dane uzytkownika",
            () -> assertEquals("Jan", user.getImie(), "Imie nieprawidlowe"),
            () -> assertEquals("Kowalski", user.getNazwisko(), "Nazwisko nieprawidlowe"),
            () -> assertEquals(30, user.getWiek(), "Wiek nieprawidlowy"),
            () -> assertNotNull(user.getEmail(), "Email nie moze byc null")
        );
    }

    @Test
    void zagniezdzoneAssertAll() {
        Zamowienie z = serwis.pobierz(42L);

        // Hierarchiczna weryfikacja - czytelny raport bledow
        assertAll("Zamowienie",
            () -> assertNotNull(z.getKlient(), "Klient nie moze byc null"),
            () -> assertAll("Adres dostawy",
                () -> assertEquals("Warszawa", z.getAdres().getMiasto()),
                () -> assertEquals("00-001", z.getAdres().getKod())
            ),
            () -> assertAll("Pozycje",
                () -> assertFalse(z.getPozycje().isEmpty(), "Brak pozycji"),
                () -> assertTrue(z.getSuma() > 0, "Suma musi byc dodatnia")
            )
        );
    }
}

Materiały

Jak testować wyjątki za pomocą assertThrows i assertThrowsExactly?

Odpowiedź w 30 sekund: assertThrows(Class, Executable) sprawdza, czy wykonanie lambdy rzuca wyjątek danego typu LUB jego podklasy i zwraca instancję wyjątku do dalszej weryfikacji (np. komunikatu). assertThrowsExactly wymaga DOKŁADNIE tego typu — podklasa spowoduje porażkę. Używaj pierwszego dla większości przypadków, drugiego gdy hierarchia wyjątków ma znaczenie.

Odpowiedź w 2 minuty: Testowanie wyjątków to częsty wymóg: weryfikacja walidacji wejścia, błędów biznesowych, kontraktów API. JUnit 5 oferuje dwie metody:

  • assertThrows(Class<T>, Executable) — asercja przechodzi, jeśli lambda rzuci wyjątek typu T lub jego podklasy. To zachowanie zgodne z polimorfizmem Javy: IllegalArgumentException przechodzi test dla assertThrows(RuntimeException.class, ...). Metoda zwraca rzucony wyjątek, dzięki czemu można sprawdzić jego komunikat, przyczynę (getCause) czy pola.
  • assertThrowsExactly(Class<T>, Executable) (od JUnit 5.8) — wymaga dokładnego dopasowania typu. IllegalArgumentException NIE przejdzie testu dla assertThrowsExactly(RuntimeException.class, ...). Używaj, gdy ważne jest, by kod rzucał konkretną klasę, a nie jej nadtyp (np. własny OrderValidationException, a nie surowy RuntimeException).
Metoda Akceptuje podklasę? Kiedy używać
assertThrows(RuntimeException.class, ...) Tak — IllegalArgumentException, NullPointerException itp. przejdą Domyślnie, gdy interesuje nas rodzina wyjątków
assertThrowsExactly(RuntimeException.class, ...) Nie — tylko dokładnie RuntimeException Gdy kontrakt mówi: "rzuć dokładnie ten typ"

Częsta dobra praktyka: po wyłapaniu wyjątku zweryfikuj jego komunikat lub pola — sam fakt rzucenia wyjątku to za mało, bo wyjątek mógłby być rzucony z innego powodu, niż się spodziewamy.

Przykład kodu:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class WalidatorTest {

    @Test
    void powinienRzuceIllegalArgumentDlaPustegoLogin() {
        Walidator walidator = new Walidator();

        // assertThrows zwraca instancje wyjatku - mozemy ja zweryfikowac
        IllegalArgumentException wyjatek = assertThrows(
            IllegalArgumentException.class,
            () -> walidator.sprawdzLogin(""),
            "Powinien rzucic IllegalArgumentException dla pustego loginu"
        );

        // Weryfikacja komunikatu - klucz do dobrego testu wyjatkow
        assertEquals("Login nie moze byc pusty", wyjatek.getMessage());
        assertTrue(wyjatek.getMessage().contains("pusty"));
    }

    @Test
    void roznicaMiedzyAssertThrowsAAssertThrowsExactly() {
        // BankAccountException dziedziczy po RuntimeException
        // assertThrows - PRZECHODZI bo BankAccountException jest podklasa RuntimeException
        assertThrows(RuntimeException.class,
            () -> konto.wyplac(1_000_000));

        // assertThrowsExactly - NIE PRZECHODZI bo to nie jest dokladnie RuntimeException
        // assertThrowsExactly(RuntimeException.class,
        //     () -> konto.wyplac(1_000_000));  // <- FAIL: oczekiwano RuntimeException, rzucono BankAccountException

        // Dokladne sprawdzenie wlasnego typu wyjatku
        BankAccountException ex = assertThrowsExactly(
            BankAccountException.class,
            () -> konto.wyplac(1_000_000),
            "Oczekiwano dokladnie BankAccountException"
        );
        assertEquals("Brak srodkow", ex.getMessage());
        assertNotNull(ex.getCause(), "Powinna byc przyczyna");
    }

    @Test
    void weryfikacjaWlasnychPolWyjatku() {
        OrderValidationException ex = assertThrows(
            OrderValidationException.class,
            () -> serwis.zatwierdz(zlePodanieZamowienia)
        );

        // Wlasne pola wyjatku - czesto bardziej wartosciowe niz komunikat
        assertEquals("ORD-001", ex.getKodBledu());
        assertEquals(3, ex.getNiepoprawnePola().size());
    }
}

Materiały

Jak używać assertTimeout vs assertTimeoutPreemptively i jakie są pułapki?

Odpowiedź w 30 sekund: assertTimeout wykonuje kod w tym samym wątku i czeka, aż się zakończy — dopiero potem zgłasza porażkę, jeśli przekroczył limit. assertTimeoutPreemptively uruchamia kod w osobnym wątku i przerywa go, gdy przekroczy timeout. Drugi wariant jest niebezpieczny przy kodzie używającym ThreadLocal, transakcji Springa czy zasobów wątkowych, bo działa w innym wątku.

Odpowiedź w 2 minuty: JUnit 5 oferuje dwa sposoby testowania, czy operacja zmieści się w zadanym czasie:

  • assertTimeout(Duration, Executable) — wywołuje kod w wątku testu, mierzy czas i jeśli przekroczy limit, raportuje porażkę. Nie przerywa wykonania — czeka, aż kod się zakończy. To bezpieczne pod względem zachowania kontekstu wątku, ale jeśli kod ma nieskończoną pętlę, test też zawiśnie aż do timeoutu surefire/gradle.

  • assertTimeoutPreemptively(Duration, Executable) — uruchamia kod w osobnym wątku i jeśli ten nie zakończy się w limicie, przerywa go (interrupt) i zgłasza porażkę natychmiast. Brzmi lepiej, ale ma poważne pułapki: kod działa w innym wątku, więc traci ThreadLocal, kontekst transakcji Springa (TransactionSynchronizationManager używa ThreadLocal!), SecurityContextHolder w Spring Security, kontekst MDC w logach itp.

Cecha assertTimeout assertTimeoutPreemptively
Wątek wykonania Ten sam co test Osobny wątek
Reakcja na timeout Czeka, raportuje po zakończeniu Przerywa wątek (interrupt)
ThreadLocal / Transakcja Springa Działa normalnie Utrata kontekstu — pułapka!
Test z nieskończoną pętlą Zawiesza się Przerywa po timeoucie
Bezpieczeństwo Wysokie Niskie (efekty uboczne przerwania)

Reguła praktyczna: zaczynaj od assertTimeout. Sięgnij po assertTimeoutPreemptively tylko gdy musisz zatrzymać wiszący kod (np. test integracyjny z deadlockiem) i wiesz, że testowany kod jest "thread-agnostic" — nie używa ThreadLocal, transakcji ani innych zasobów wiązanych z wątkiem.

Przykład kodu:

import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;

class WydajnoscTest {

    @Test
    void powinienZakonczySieWCzasie_bezpiecznyWariant() {
        // assertTimeout - kod w tym samym watku, ThreadLocal/transakcje OK
        // ALE: jesli kod sie zawiesi, test bedzie wisial
        String wynik = assertTimeout(
            Duration.ofMillis(500),
            () -> serwis.szybkaOperacja(),
            "Operacja powinna zakonczyc sie w 500ms"
        );
        assertEquals("OK", wynik);
    }

    @Test
    void powinienPrzerwaeDlugiKod_szybkiAleNiebezpieczny() {
        // assertTimeoutPreemptively - kod w osobnym watku, przerywa po timeoucie
        // UWAGA: ThreadLocal i transakcje Springa NIE PRZECHODZA
        assertTimeoutPreemptively(
            Duration.ofMillis(100),
            () -> {
                // To bedzie przerwane jesli nie skonczy sie w 100ms
                serwis.operacjaKtoraMozeSieZawiesic();
            }
        );
    }

    @Test
    void pulapka_transakcjaSpringa_NIE_dziala_z_preemptively() {
        // Transactional kontekst jest w ThreadLocal - w osobnym watku go nie ma!
        // To NIE ZADZIALA poprawnie:
        // assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        //     repozytorium.zapisz(encja);  // brak aktywnej transakcji w osobnym watku!
        // });

        // Wlasciwy wybor: assertTimeout
        assertTimeout(Duration.ofSeconds(1), () -> {
            repozytorium.zapisz(encja);  // dziala w watku testu - transakcja zachowana
        });
    }

    @Test
    void pulapka_threadLocalMDC() {
        MDC.put("requestId", "REQ-123");

        // Logi w osobnym watku NIE BEDA mialy MDC requestId!
        // assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        //     serwis.zaloguj("test");  // log bez requestId
        // });

        // Z assertTimeout MDC jest dostepne
        assertTimeout(Duration.ofSeconds(1), () -> {
            serwis.zaloguj("test");  // log z requestId=REQ-123
        });
    }
}

Materiały

Czym jest AssertJ i dlaczego często wybiera się go zamiast JUnit Assertions?

Odpowiedź w 30 sekund: AssertJ to biblioteka asercji oferująca fluent API: assertThat(actual).matcher1().matcher2().... W porównaniu do JUnit Assertions daje znacznie więcej matcherów dla kolekcji, dat, plików, wyjątków, świetne komunikaty błędów i czytelność zbliżoną do języka naturalnego. Łączy się z JUnit (5/4) i jest niemal standardem w nowoczesnych projektach Java.

Odpowiedź w 2 minuty: JUnit Assertions to fasada statyczna z ograniczoną liczbą metod. Dobrze radzi sobie z prostymi przypadkami (assertEquals, assertTrue), ale przy złożonych asercjach na kolekcjach, mapach, datach czy wyjątkach wymusza pisanie wielu linijek, gdzie każda sprawdza jeden warunek.

AssertJ (assertj-core) zmienia podejście: jedna metoda startowa assertThat(actual) zwraca specjalizowany "Assert" dla danego typu (np. ListAssert, MapAssert, OptionalAssert, LocalDateAssert, ThrowableAssert), a po niej chainujemy dziesiątki matcherów. Każdy zwraca this, więc można łączyć asercje w jedną czytelną linię, która brzmi prawie jak zdanie.

Cecha JUnit Assertions AssertJ
Styl API Statyczne metody Fluent / chainable
Czytelność assertEquals(3, list.size()) assertThat(list).hasSize(3)
Asercje na kolekcjach Kilka podstawowych contains, containsExactly, containsExactlyInAnyOrder, extracting, filteredOn...
Asercje na datach Brak dedykowanych isBefore, isAfter, isCloseTo, hasYear...
Asercje na wyjątkach assertThrows + ręczna weryfikacja assertThatThrownBy(...).isInstanceOf(...).hasMessage(...).hasCauseInstanceOf(...)
Komunikaty błędów Krótkie, surowe Bardzo szczegółowe, z różnicami pól
Custom matchery Trudne .satisfies(c -> ...), .as(...), .usingRecursiveComparison()
Soft assertions Tylko assertAll SoftAssertions / assertSoftly

Najmocniejsze cechy AssertJ: porównanie obiektów po wszystkich polach (usingRecursiveComparison() zamiast pisania equals), wyciąganie pól z kolekcji (extracting("imie")), opisy (.as("Klient %d", id)) dodawane do komunikatów błędów. Dlatego w projektach Spring/microservices to faktyczny standard, mimo że trzeba dodać zależność org.assertj:assertj-core:3.x.

Przykład kodu:

import org.junit.jupiter.api.Test;
import java.util.List;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.*;

class AssertJTest {

    @Test
    void porownanieZJUnitAssertions() {
        List<Integer> liczby = List.of(1, 2, 3);

        // JUnit Assertions - wiele linii, malo czytelne
        // assertEquals(3, liczby.size());
        // assertTrue(liczby.contains(2));
        // assertEquals(Integer.valueOf(1), liczby.get(0));

        // AssertJ - fluent, czytelne jak zdanie
        assertThat(liczby)
            .hasSize(3)
            .containsExactly(1, 2, 3)
            .doesNotContain(0, 4);
    }

    @Test
    void asercjeNaKolekcjachObiektow() {
        List<Uzytkownik> uzytkownicy = serwis.pobierzWszystkich();

        // Wyciaganie pol z kolekcji + asercje na wynikach
        assertThat(uzytkownicy)
            .hasSize(3)
            .extracting("imie")
            .containsExactlyInAnyOrder("Jan", "Anna", "Piotr");

        // Filtrowanie i sprawdzanie
        assertThat(uzytkownicy)
            .filteredOn(u -> u.getWiek() > 30)
            .hasSize(1)
            .first()
            .extracting(Uzytkownik::getImie)
            .isEqualTo("Piotr");
    }

    @Test
    void asercjeNaWyjatkach_zwarteWJednejLinii() {
        // Klasyczny JUnit:
        // var ex = assertThrows(IllegalArgumentException.class, () -> walidator.sprawdz(""));
        // assertEquals("Pusta wartosc", ex.getMessage());

        // AssertJ - jedna fluent asercja
        assertThatThrownBy(() -> walidator.sprawdz(""))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("Pusta wartosc")
            .hasMessageContaining("Pusta")
            .hasCauseInstanceOf(NullPointerException.class);
    }

    @Test
    void asercjeNaDatach() {
        LocalDate dataZamowienia = serwis.dataZamowienia(42L);

        assertThat(dataZamowienia)
            .isAfter(LocalDate.of(2025, 1, 1))
            .isBefore(LocalDate.now())
            .hasYear(2025)
            .isInThePast();
    }

    @Test
    void porownanieRekurencyjne_zamiast_equals() {
        Uzytkownik oczekiwany = new Uzytkownik("Jan", "Kowalski", 30);
        Uzytkownik aktualny = serwis.znajdz(1L);

        // Nie wymaga equals() - porownuje wszystkie pola refleksja
        assertThat(aktualny)
            .usingRecursiveComparison()
            .ignoringFields("dataModyfikacji")
            .isEqualTo(oczekiwany);
    }

    @Test
    void softAssertions_jak_assertAll_ale_ladniej() {
        Uzytkownik u = serwis.znajdz(1L);

        assertSoftly(softly -> {
            softly.assertThat(u.getImie()).isEqualTo("Jan");
            softly.assertThat(u.getNazwisko()).isEqualTo("Kowalski");
            softly.assertThat(u.getWiek()).isEqualTo(30);
            // wszystkie bledy zgloszone razem na koncu
        });
    }
}

Materiały

Lifecycle i hooks

Czym różnią się @BeforeEach, @AfterEach, @BeforeAll i @AfterAll?

Odpowiedź w 30 sekund: @BeforeEach i @AfterEach uruchamiają się przed i po każdej metodzie testowej, służąc do przygotowywania i sprzątania stanu indywidualnego dla pojedynczego testu. @BeforeAll i @AfterAll uruchamiają się raz na całą klasę testową (przed pierwszym i po ostatnim teście) i służą do kosztownej inicjalizacji wspólnej, np. uruchomienia kontenera bazy danych czy serwera HTTP.

Odpowiedź w 2 minuty: JUnit 5 wprowadza cztery podstawowe adnotacje cyklu życia testu. @BeforeEach (zastępuje @Before z JUnit 4) wykonuje się przed każdą metodą @Test, co gwarantuje że każdy test startuje z czystym, świeżo zainicjalizowanym stanem. Analogicznie @AfterEach (zastępuje @After) wykonuje się po każdym teście, niezależnie od jego wyniku (sukces, niepowodzenie, wyjątek) — idealnie nadaje się do zamykania zasobów i resetowania stanu.

@BeforeAll (zastępuje @BeforeClass) i @AfterAll (zastępuje @AfterClass) wykonują się tylko raz na klasę testową. @BeforeAll przed pierwszą metodą testową, a @AfterAll po ostatniej. Domyślnie obie te metody muszą być statyczne, ponieważ JUnit tworzy nową instancję klasy testowej dla każdego testu — w momencie wywoływania @BeforeAll instancja jeszcze nie istnieje.

Typowy wzorzec użycia: w @BeforeAll startujemy ciężkie zasoby (Testcontainers, embedded H2, serwer Mock), w @BeforeEach resetujemy stan między testami (czyścimy tabele, resetujemy mocki), w @AfterEach sprzątamy zasoby per-test, a w @AfterAll zatrzymujemy ciężkie zasoby. Pamiętaj o izolacji — nadużywanie pól statycznych w @BeforeAll może prowadzić do testów zależnych od siebie, co łamie zasadę niezależności testów.

Diagram sekwencji:

sequenceDiagram
    participant J as JUnit
    participant K as Klasa testowa
    J->>K: @BeforeAll (raz)
    loop Dla każdego testu
        J->>K: @BeforeEach
        J->>K: @Test
        J->>K: @AfterEach
    end
    J->>K: @AfterAll (raz)

Przykład kodu:

import org.junit.jupiter.api.*;

class CyklZyciaTest {

    // Wykonuje się raz, przed wszystkimi testami
    @BeforeAll
    static void przygotujKosztowneZasoby() {
        System.out.println("Start kontenera bazy danych");
    }

    // Wykonuje się przed każdym testem
    @BeforeEach
    void przygotujStanTestu() {
        System.out.println("Reset stanu przed testem");
    }

    @Test
    void pierwszyTest() {
        System.out.println("Test 1");
    }

    @Test
    void drugiTest() {
        System.out.println("Test 2");
    }

    // Wykonuje się po każdym teście
    @AfterEach
    void posprzatajPoTescie() {
        System.out.println("Sprzątanie po teście");
    }

    // Wykonuje się raz, po wszystkich testach
    @AfterAll
    static void zatrzymajZasoby() {
        System.out.println("Zamknięcie bazy danych");
    }
}
// Kolejność: BeforeAll → BeforeEach → Test1 → AfterEach
//            → BeforeEach → Test2 → AfterEach → AfterAll

Materiały

Dlaczego @BeforeAll i @AfterAll muszą być statyczne (i kiedy nie)?

Odpowiedź w 30 sekund: Domyślnie @BeforeAll i @AfterAll muszą być statyczne, ponieważ JUnit 5 domyślnie tworzy nową instancję klasy testowej dla każdej metody testowej (tryb PER_METHOD) — w momencie wywołania @BeforeAll żadna instancja jeszcze nie istnieje. Wyjątek: użycie @TestInstance(Lifecycle.PER_CLASS) pozwala na metody niestatyczne, bo wtedy istnieje jedna współdzielona instancja.

Odpowiedź w 2 minuty: Domyślnym trybem cyklu życia w JUnit 5 jest Lifecycle.PER_METHOD, co oznacza, że framework tworzy nową instancję klasy testowej dla każdej metody @Test. Ma to chronić testy przed niepożądanym dzieleniem stanu poprzez pola obiektu — każdy test dostaje świeży obiekt. Konsekwencją tej decyzji jest jednak fakt, że metody @BeforeAll i @AfterAll, które mają wykonać się przed jakimkolwiek testem i po wszystkich testach, nie mogą być powiązane z żadną konkretną instancją. Dlatego muszą być statyczne — wywoływane na poziomie klasy, nie obiektu.

Jeśli zapomnimy słowa static, JUnit rzuci wyjątek JUnitException: @BeforeAll method ... must be static. To częsty błąd początkujących migrujących z JUnit 4 (gdzie @BeforeClass również musi być statyczny — zasada jest tożsama).

Wyjątek od reguły — można uczynić te metody niestatycznymi, oznaczając klasę adnotacją @TestInstance(TestInstance.Lifecycle.PER_CLASS). Wtedy JUnit utworzy jedną instancję klasy testowej dla wszystkich metod, więc metody hookowe mogą operować na polach obiektu. To wygodne np. przy testach parametryzowanych z @MethodSource, gdzie metoda dostarczająca dane też nie musi być wtedy statyczna. Trzeba jednak uważać — współdzielona instancja oznacza, że pola obiektu są dzielone między testami, co może prowadzić do testów zależnych od kolejności, jeśli nie zachowamy dyscypliny.

Przykład kodu:

import org.junit.jupiter.api.*;

// Wariant 1: domyślny PER_METHOD — metody muszą być static
class WariantDomyslnyTest {

    @BeforeAll
    static void przygotuj() {
        // Musi być static — instancja klasy jeszcze nie istnieje
        System.out.println("Inicjalizacja na poziomie klasy");
    }

    @AfterAll
    static void zakoncz() {
        // Musi być static — z tych samych powodów
        System.out.println("Sprzątanie na poziomie klasy");
    }

    @Test
    void test1() { /* ... */ }
}

// Wariant 2: PER_CLASS — metody mogą być niestatyczne
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WariantPerClassTest {

    private String wspolnyZasob;

    @BeforeAll
    void przygotuj() {
        // Brak 'static'! Działa, bo istnieje jedna instancja klasy
        wspolnyZasob = "zainicjalizowany";
    }

    @AfterAll
    void zakoncz() {
        // Również może operować na polach instancji
        wspolnyZasob = null;
    }

    @Test
    void testKorzystaZWspolnegoZasobu() {
        Assertions.assertNotNull(wspolnyZasob);
    }
}

Materiały

Jak działa @TestInstance(Lifecycle.PER_CLASS) i jakie zmienia zachowanie?

Odpowiedź w 30 sekund: @TestInstance(Lifecycle.PER_CLASS) zmienia domyślne zachowanie JUnit 5 z tworzenia nowej instancji klasy testowej dla każdej metody testowej (PER_METHOD) na tworzenie jednej współdzielonej instancji dla całej klasy. Pozwala to na niestatyczne metody @BeforeAll, @AfterAll i @MethodSource, a także na współdzielenie stanu w polach instancji między testami.

Odpowiedź w 2 minuty: Domyślnie JUnit 5 stosuje Lifecycle.PER_METHOD — każda metoda testowa otrzymuje świeżą instancję klasy testowej. Ta filozofia zapewnia maksymalną izolację testów: pola obiektu są zawsze w stanie początkowym, niezależnie od kolejności testów. Cena to konieczność deklarowania @BeforeAll/@AfterAll/@MethodSource jako statycznych.

Po dodaniu @TestInstance(TestInstance.Lifecycle.PER_CLASS) na poziomie klasy testowej JUnit tworzy jedną instancję klasy i wykorzystuje ją do wykonania wszystkich metod testowych. Konsekwencje:

  1. Metody hookowe mogą być niestatyczne — @BeforeAll, @AfterAll oraz źródła danych dla testów parametryzowanych (@MethodSource) mogą operować na polach obiektu.
  2. Pola instancji są współdzielone — stan zapisany w jednym teście jest widoczny w kolejnych. To może być pomocne (np. cache wspólnych obiektów) lub niebezpieczne (testy zależne od siebie).
  3. Lepsza ergonomia dla testów @Nested — klasy zagnieżdżone nie muszą być statyczne i mogą odwoływać się do zewnętrznej klasy.
  4. Mniejszy narzut alokacji — przy bardzo dużej liczbie testów oszczędzamy na tworzeniu obiektów (rzadko ma to znaczenie w praktyce).

Kiedy używać PER_CLASS:

  • Gdy potrzebujemy niestatycznych metod @MethodSource/@BeforeAll.
  • Gdy świadomie projektujemy testy z dzielonym stanem (np. budowa fluent buildera).
  • W testach @Nested z bogatą hierarchią.

Kiedy unikać:

  • Gdy testy mają być całkowicie niezależne (lepsza izolacja w PER_METHOD).
  • Gdy testy mogą wpływać na siebie nawzajem przez wspólne pola.

Można ustawić tryb globalnie poprzez plik junit-platform.properties: junit.jupiter.testinstance.lifecycle.default = per_class.

Przykład kodu:

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestInstancePerClassTest {

    // Pole instancji — współdzielone między testami
    private int licznikWywolan = 0;

    // Niestatyczne @BeforeAll — niemożliwe w PER_METHOD
    @BeforeAll
    void inicjalizacja() {
        System.out.println("Jeden raz, na tej samej instancji");
    }

    // Niestatyczna metoda dostarczająca dane do testów parametryzowanych
    Stream<String> daneTestowe() {
        return Stream.of("Anna", "Jan", "Maria");
    }

    @ParameterizedTest
    @MethodSource("daneTestowe")
    void testZParametrem(String imie) {
        licznikWywolan++; // Współdzielone pole — narasta między testami
        Assertions.assertNotNull(imie);
    }

    @AfterAll
    void podsumowanie() {
        System.out.println("Liczba wywołań: " + licznikWywolan);
        // Tu pole instancji nadal jest dostępne
    }
}

Materiały

W jakiej kolejności wykonują się hooki w klasach zagnieżdżonych @Nested?

Odpowiedź w 30 sekund: W klasach zagnieżdżonych @Nested hooki wykonują się od zewnątrz do wewnątrz przed testem i od wewnątrz na zewnątrz po teście: zewnętrzny @BeforeAll → zewnętrzny @BeforeEach → wewnętrzny @BeforeEach → test → wewnętrzny @AfterEach → zewnętrzny @AfterEach → zewnętrzny @AfterAll. Klasy @Nested mogą mieć własne @BeforeAll/@AfterAll tylko z @TestInstance(PER_CLASS).

Odpowiedź w 2 minuty: Klasy zagnieżdżone @Nested w JUnit 5 pozwalają grupować logicznie powiązane testy w hierarchię, co znacząco poprawia czytelność i wyrazistość testów (idealne dla stylu BDD z @DisplayName). Hooki wykonują się zgodnie z zasadą zewnętrzne owijają wewnętrzne — analogicznie jak warstwy cebuli lub konstruktor/destruktor klas dziedziczących.

Kolejność dla pojedynczego testu w klasie zagnieżdżonej:

  1. @BeforeAll klasy zewnętrznej (raz, na początku).
  2. @BeforeEach klasy zewnętrznej (przed każdym testem, też tym zagnieżdżonym).
  3. @BeforeAll klasy zagnieżdżonej (jeśli istnieje — wymaga PER_CLASS na klasie zagnieżdżonej).
  4. @BeforeEach klasy zagnieżdżonej.
  5. Metoda testowa.
  6. @AfterEach klasy zagnieżdżonej.
  7. @AfterEach klasy zewnętrznej.
  8. @AfterAll klasy zagnieżdżonej (jeśli istnieje).
  9. @AfterAll klasy zewnętrznej (raz, na końcu wszystkich testów).

Ważne ograniczenie: klasy @Nested są domyślnie niestatyczne (instancja klasy wewnętrznej wymaga referencji do klasy zewnętrznej). To powoduje, że @BeforeAll/@AfterAll w klasie zagnieżdżonej nie mogą być statyczne (klasa wewnętrzna niestatyczna nie może mieć statycznych członków), więc wymagają adnotacji @TestInstance(Lifecycle.PER_CLASS) na klasie zagnieżdżonej. Bez niej JUnit zignoruje te hooki lub zgłosi błąd.

Typowe zastosowanie: zewnętrzna klasa testowa konfiguruje wspólny kontekst (np. tworzy serwis), a klasy zagnieżdżone testują różne scenariusze biznesowe (np. "Gdy użytkownik jest zalogowany", "Gdy użytkownik nie ma uprawnień"), każda z własnym specyficznym setupem w @BeforeEach.

Diagram przepływu hooków:

flowchart TD
    A[Zewnętrzny @BeforeAll] --> B[Zewnętrzny @BeforeEach]
    B --> C[Wewnętrzny @BeforeEach]
    C --> D[Test w @Nested]
    D --> E[Wewnętrzny @AfterEach]
    E --> F[Zewnętrzny @AfterEach]
    F --> G{Kolejny test?}
    G -->|Tak| B
    G -->|Nie| H[Zewnętrzny @AfterAll]

Przykład kodu:

import org.junit.jupiter.api.*;

class HookiZagnieżdżoneTest {

    @BeforeAll
    static void zewnętrzny_BeforeAll() {
        System.out.println("1. Zewnętrzny @BeforeAll");
    }

    @BeforeEach
    void zewnętrzny_BeforeEach() {
        System.out.println("2. Zewnętrzny @BeforeEach");
    }

    @Test
    void testNaZewnętrznymPoziomie() {
        System.out.println("   -> Test zewnętrzny");
    }

    @AfterEach
    void zewnętrzny_AfterEach() {
        System.out.println("4. Zewnętrzny @AfterEach");
    }

    @AfterAll
    static void zewnętrzny_AfterAll() {
        System.out.println("5. Zewnętrzny @AfterAll");
    }

    @Nested
    class GdyUżytkownikJestZalogowany {

        @BeforeEach
        void wewnętrzny_BeforeEach() {
            System.out.println("   2b. Wewnętrzny @BeforeEach");
        }

        @Test
        void testNaWewnętrznymPoziomie() {
            System.out.println("      -> Test zagnieżdżony");
        }

        @AfterEach
        void wewnętrzny_AfterEach() {
            System.out.println("   3b. Wewnętrzny @AfterEach");
        }
    }
}

// Kolejność wykonania dla testu zagnieżdżonego:
// 1. Zewnętrzny @BeforeAll
// 2. Zewnętrzny @BeforeEach
//    2b. Wewnętrzny @BeforeEach
//       -> Test zagnieżdżony
//    3b. Wewnętrzny @AfterEach
// 4. Zewnętrzny @AfterEach
// 5. Zewnętrzny @AfterAll

Materiały

Parametryzowane testy

Czym są testy parametryzowane (@ParameterizedTest) i kiedy warto ich używać?

Odpowiedź w 30 sekund: @ParameterizedTest to mechanizm JUnit 5 pozwalający uruchomić ten sam test wielokrotnie z różnymi zestawami danych wejściowych. Eliminuje powielanie kodu testowego i jest idealny do weryfikacji logiki na wielu przypadkach brzegowych w jednej metodzie testowej.

Odpowiedź w 2 minuty: Testy parametryzowane zastępują wiele niemal identycznych metod testowych jedną metodą, która przyjmuje argumenty z określonego źródła (np. @ValueSource, @CsvSource, @MethodSource). Każdy zestaw argumentów tworzy osobne wywołanie testu z własnym wynikiem (pass/fail) widocznym w raporcie, co znacznie ułatwia diagnostykę.

Warto ich używać do testowania funkcji walidacyjnych (różne formaty e-maila, numerów telefonu), obliczeń (różne wartości wejściowe i oczekiwane), enumów, parserów, czy konwerterów. Pomagają zachować zasadę DRY i pokrywać przypadki brzegowe (null, puste stringi, wartości skrajne) bez tworzenia osobnych testów.

WAŻNE: Testy parametryzowane wymagają osobnego modułu junit-jupiter-params — nie wystarczy junit-jupiter w pełnej wersji. Bez tej zależności adnotacje @ParameterizedTest i źródła argumentów nie będą dostępne.

Przykład kodu:

// Dependency w pom.xml:
// <dependency>
//   <groupId>org.junit.jupiter</groupId>
//   <artifactId>junit-jupiter-params</artifactId>
//   <version>5.10.2</version>
//   <scope>test</scope>
// </dependency>

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;

class StringUtilsTest {

    // Zamiast 4 osobnych testów — jeden parametryzowany
    @ParameterizedTest(name = "Test {index}: \"{0}\" nie jest puste")
    @ValueSource(strings = { "abc", " a ", "hello", "JUnit" })
    void shouldDetectNonBlankStrings(String input) {
        // Każda wartość uruchamia test osobno
        assertTrue(input != null && !input.isBlank());
    }
}

Materiały

21. Jakie są źródła argumentów (@ValueSource, @CsvSource, @MethodSource, @EnumSource)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
22. Jak używać @CsvFileSource i @ArgumentsSource dla złożonych danych testowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
23. Czym jest @RepeatedTest i jak różni się od testów parametryzowanych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
24. Jak skonfigurować własny ArgumentsProvider dla niestandardowych scenariuszy?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Testy zagnieżdżone i ordering

25. Czym jest @Nested i jakie korzyści daje zagnieżdżanie klas testowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
26. Jak zapewnić kolejność wykonywania testów (@TestMethodOrder, @Order)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
27. Czym jest @TestInstance i jak wpływa na cykl życia instancji klasy?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
28. Czy testy powinny być od siebie zależne — jakie są kompromisy ordering vs izolacja?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Extensions

29. Czym jest mechanizm Extensions w JUnit 5 i jak zastąpił JUnit 4 @RunWith/@Rule?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
30. Jak utworzyć własny Extension (np. logger, timing, retry)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
31. Czym jest ParameterResolver i jak wstrzykiwać zależności do metod testowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
32. Jak działa @RegisterExtension i kiedy używać go zamiast @ExtendWith?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
33. Czym są lifecycle callbacks (BeforeEachCallback, AfterTestExecutionCallback)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Integracja z Mockito

34. Jak skonfigurować Mockito z JUnit 5 (@ExtendWith(MockitoExtension.class))?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
35. Czym różni się @Mock od @Spy i kiedy używać każdego z nich?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
36. Do czego służy @InjectMocks i jakie ma ograniczenia (constructor vs setter vs field injection)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
37. Jak weryfikować interakcje z mockami (verify, times, never, inOrder)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
38. Jak stubować metody (when().thenReturn(), doReturn().when()) i kiedy używać której formy?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Spring Boot Test

39. Czym jest @SpringBootTest i jakie tryby (webEnvironment) udostępnia?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
40. Jak działają test slices (@WebMvcTest, @DataJpaTest, @JsonTest)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
41. Czym różni się @MockBean od @SpyBean i jak współpracują ze Spring Context?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
42. Jak testować REST API z MockMvc i TestRestTemplate?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
43. Jak testować warstwę repozytorium z @DataJpaTest i Testcontainers?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Wzorce i strategie testowe

44. Jak stosować zasadę AAA (Arrange-Act-Assert) w testach JUnit 5?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
45. Czym są fixtures, builders i Object Mother — jak organizować dane testowe?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
46. Jakie są dobre praktyki nazewnictwa testów (Given-When-Then, should-when, polskie konwencje)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
47. Jak izolować testy jednostkowe od integracyjnych (tagi, profile, slices)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
48. Jak testować kod asynchroniczny i wielowątkowy w JUnit 5?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Wydajność i równoległość

49. Jak włączyć równoległe wykonywanie testów w JUnit 5 (junit.jupiter.execution.parallel.enabled)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
50. Jakie są strategie wykonania równoległego (SAME_THREAD vs CONCURRENT)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
51. Jak diagnozować wolne testy i optymalizować suitę testów?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Migracja i najlepsze praktyki

52. Jakie są największe pułapki migracji z JUnit 4 do JUnit 5?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
53. Jak uruchamiać razem testy JUnit 4 i JUnit 5 w jednym projekcie (junit-vintage-engine)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
54. Jakie są typowe antywzorce w testach JUnit 5 (testowanie szczegółów implementacji, over-mocking)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
55. Jak integrować JUnit 5 z CI/CD (raporty, coverage, fail-fast)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Chcesz poznać wszystkie odpowiedzi?

Uzyskaj pełny dostęp do 55 pytań rekrutacyjnych z JUnit 5 oraz pozostałych technologii.

Zobacz plany cenowe