Pytania rekrutacyjne JUnit 5
55 pytań — 20 odpowiedzi dostępnych od razu
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 —
@IntegrationTestjako 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će2eitd.
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["<groups> fast<br/><excludedGroups> 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 zThreadLocal).assertTimeoutPreemptively— gdy chcesz "twardo" przerywać i nie używaszThreadLocal-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 typuTlub jego podklasy. To zachowanie zgodne z polimorfizmem Javy:IllegalArgumentExceptionprzechodzi test dlaassertThrows(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.IllegalArgumentExceptionNIE przejdzie testu dlaassertThrowsExactly(RuntimeException.class, ...). Używaj, gdy ważne jest, by kod rzucał konkretną klasę, a nie jej nadtyp (np. własnyOrderValidationException, a nie surowyRuntimeException).
| 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 traciThreadLocal, kontekst transakcji Springa (TransactionSynchronizationManagerużywa ThreadLocal!),SecurityContextHolderw 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:
- Metody hookowe mogą być niestatyczne —
@BeforeAll,@AfterAlloraz źródła danych dla testów parametryzowanych (@MethodSource) mogą operować na polach obiektu. - 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).
- Lepsza ergonomia dla testów
@Nested— klasy zagnieżdżone nie muszą być statyczne i mogą odwoływać się do zewnętrznej klasy. - 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
@Nestedz 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:
@BeforeAllklasy zewnętrznej (raz, na początku).@BeforeEachklasy zewnętrznej (przed każdym testem, też tym zagnieżdżonym).@BeforeAllklasy zagnieżdżonej (jeśli istnieje — wymagaPER_CLASSna klasie zagnieżdżonej).@BeforeEachklasy zagnieżdżonej.- Metoda testowa.
@AfterEachklasy zagnieżdżonej.@AfterEachklasy zewnętrznej.@AfterAllklasy zagnieżdżonej (jeśli istnieje).@AfterAllklasy 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
@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@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@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 odpowiedziArgumentsProvider dla niestandardowych scenariuszy?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziTesty zagnieżdżone i ordering
@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@TestMethodOrder, @Order)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi@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 odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziExtensions
@RunWith/@Rule?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziExtension (np. logger, timing, retry)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziParameterResolver 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@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 odpowiedziBeforeEachCallback, AfterTestExecutionCallback)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziIntegracja z Mockito
@ExtendWith(MockitoExtension.class))?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi@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@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 odpowiedziverify, times, never, inOrder)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziwhen().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 odpowiedziSpring Boot Test
@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@WebMvcTest, @DataJpaTest, @JsonTest)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi@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 odpowiedziMockMvc i TestRestTemplate?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi@DataJpaTest i Testcontainers?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziWzorce i strategie testowe
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziWydajność i równoległość
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 odpowiedziSAME_THREAD vs CONCURRENT)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziMigracja i najlepsze praktyki
Odpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 35 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi