Fiszki Online JUnit 5 (Preview)
Darmowy podgląd 15 z 55 dostępnych pytań
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
↑ Powrót na górę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
↑ Powrót na górę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
↑ Powrót na górę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
↑ Powrót na górę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
↑ Powrót na góręParametryzowane testy
Jakie są źródła argumentów (@ValueSource, @CsvSource, @MethodSource, @EnumSource)?
Odpowiedź w 30 sekund:
JUnit 5 oferuje wiele adnotacji dostarczających argumenty: @ValueSource dla prostych literałów, @CsvSource dla wielu argumentów w formacie CSV, @MethodSource dla danych ze statycznej metody, @EnumSource dla wartości enumów, plus @CsvFileSource i @ArgumentsSource. Wybór zależy od liczby i złożoności argumentów.
Odpowiedź w 2 minuty:
@ValueSource to najprostsze źródło — przyjmuje tablicę literałów jednego typu (ints, longs, doubles, strings, classes). Działa tylko dla pojedynczego argumentu.
@CsvSource pozwala podać wiele argumentów na test w formacie CSV inline ("input,expected"). Idealne dla 2–4 argumentów typu prostego z konwersją automatyczną.
@MethodSource to najbardziej elastyczne źródło — wskazuje statyczną metodę zwracającą Stream<Arguments>, Iterable, Iterator lub tablicę. Pozwala dynamicznie generować dane testowe, włącznie z obiektami złożonymi.
@EnumSource dostarcza wartości enuma — można użyć mode = EXCLUDE/INCLUDE z names = {...} aby filtrować podzbiory.
| Źródło | Kiedy używać |
|---|---|
@ValueSource |
Jeden argument typu prostego (int, String, double, class) |
@CsvSource |
2–4 argumenty proste, dane statyczne inline |
@CsvFileSource |
Duże zestawy danych w zewnętrznym pliku CSV |
@MethodSource |
Dane dynamiczne, obiekty złożone, generowanie w runtime |
@EnumSource |
Iteracja po wartościach enuma (cały lub filtrowany podzbiór) |
@ArgumentsSource |
Niestandardowa logika dostarczania danych (custom ArgumentsProvider) |
Przykład kodu:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ArgumentSourcesDemo {
// @ValueSource — pojedyncze wartości
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 5, 8})
void valueSourceDemo(int number) {
assertEquals(number, Math.abs(number));
}
// @CsvSource — wiele argumentów inline
@ParameterizedTest(name = "Test {index}: {0} + {1} = {2}")
@CsvSource({
"1, 2, 3",
"5, 5, 10",
"0, 0, 0"
})
void csvSourceDemo(int a, int b, int expected) {
assertEquals(expected, a + b);
}
// @MethodSource — dane dynamiczne ze statycznej metody
@ParameterizedTest
@MethodSource("provideTestData")
void methodSourceDemo(String input, int expectedLength) {
assertEquals(expectedLength, input.length());
}
static Stream<Arguments> provideTestData() {
return Stream.of(
Arguments.of("Java", 4),
Arguments.of("JUnit", 5),
Arguments.of("Test", 4)
);
}
// @EnumSource — wartości enuma
@ParameterizedTest
@EnumSource(value = Day.class, names = {"MONDAY", "FRIDAY"})
void enumSourceDemo(Day day) {
assertEquals(true, day.name().length() > 3);
}
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
}
Materiały
↑ Powrót na góręExtensions
Czym jest ParameterResolver i jak wstrzykiwać zależności do metod testowych?
Odpowiedź w 30 sekund:
ParameterResolver to interfejs rozszerzenia, który pozwala wstrzykiwać argumenty do konstruktora klasy testowej, metod @Test, @BeforeEach, @AfterEach itp. Implementacja musi udostępnić dwie metody: supportsParameter (czy potrafi rozwiązać dany parametr) i resolveParameter (zwróć wartość). Dzięki temu JUnit 5 wbudowanie dostarcza TestInfo, TestReporter i @TempDir, a my możemy dostarczać własne typy.
Odpowiedź w 2 minuty:
ParameterResolver to filar dependency injection w JUnit 5. W przeciwieństwie do JUnit 4, gdzie testy nie mogły mieć parametrów (poza specjalnymi runnerami), Jupiter pozwala dowolnej metodzie testowej, lifecycle (@BeforeEach, @AfterEach) i konstruktorowi klasy testowej przyjmować argumenty. Każdy argument jest rozwiązywany przez zarejestrowany ParameterResolver.
Interfejs ma dwie metody. supportsParameter(ParameterContext, ExtensionContext) zwraca true, jeśli rozszerzenie potrafi dostarczyć wartość dla danego parametru — często sprawdza się tu typ (parameterContext.getParameter().getType() == MyService.class) lub adnotację (parameterContext.isAnnotated(MyAnnotation.class)). resolveParameter(...) jest wywoływane tylko gdy supportsParameter zwróciło true i powinno zwrócić właściwą wartość.
Wbudowane rozszerzenia używające tego mechanizmu: TestInfoParameterResolver (typ TestInfo), TestReporterParameterResolver (typ TestReporter), TempDirectory (parametr Path/File oznaczony @TempDir). Mockito implementuje go w MockitoExtension — dlatego można pisać @Test void test(@Mock UserRepository repo). Spring robi to samo dla @Autowired w SpringExtension.
Ważne ograniczenia: jeśli żaden resolver nie obsługuje parametru, JUnit rzuca ParameterResolutionException. Jeśli wielu resolverów chce obsłużyć ten sam parametr, też dostaniemy wyjątek — używaj adnotacji marker żeby tego uniknąć. Wartości NIE są cachowane między parametrami w tej samej metodzie — resolveParameter jest wywoływane dla każdego parametru osobno.
Przykład kodu:
// Własny ParameterResolver wstrzykujący UserService z adnotacją @InjectService
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface InjectService {}
public class ServiceParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
// Obsługujemy tylko parametry oznaczone @InjectService typu UserService
return pc.isAnnotated(InjectService.class)
&& pc.getParameter().getType() == UserService.class;
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {
// Tworzenie nowej instancji dla każdego testu (można cachować w Store)
return new UserService(new InMemoryUserRepository());
}
}
// Użycie - wstrzykiwanie wielu typów do metody testowej
@ExtendWith(ServiceParameterResolver.class)
class UserServiceTest {
@Test
void shouldCreateUserAndSaveToTempDir(
TestInfo testInfo, // wbudowane: metadane testu
TestReporter reporter, // wbudowane: raportowanie
@TempDir Path tempDir, // wbudowane: katalog tymczasowy
@InjectService UserService userService // nasz custom resolver
) throws IOException {
// Każdy parametr rozwiązany przez inny ParameterResolver
reporter.publishEntry("test", testInfo.getDisplayName());
User user = userService.create("Anna");
Path file = tempDir.resolve("user.txt");
Files.writeString(file, user.getName());
assertTrue(Files.exists(file));
}
// Wstrzykiwanie działa też w konstruktorze i lifecycle
private final UserService service;
UserServiceTest(@InjectService UserService service) {
this.service = service;
}
@BeforeEach
void setUp(TestInfo info) {
System.out.println("Setup dla: " + info.getDisplayName());
}
}
Materiały
↑ Powrót na góręSpring Boot Test
Czym różni się @MockBean od @SpyBean i jak współpracują ze Spring Context?
Odpowiedź w 30 sekund:
@MockBean tworzy całkowity mock (atrapę) i wstrzykuje go do ApplicationContext, zastępując istniejący bean tego typu – wszystkie metody domyślnie zwracają wartości puste/zerowe. @SpyBean opakowuje prawdziwy bean w szpiega Mockito, dzięki czemu metody działają realnie, dopóki nie zastubujemy wybranych za pomocą when().thenReturn().
Odpowiedź w 2 minuty:
Obie adnotacje pochodzą z org.springframework.boot.test.mock.mockito i są integracją Mockito ze Spring Test. Kluczowa różnica leży w zachowaniu wstrzykiwanego beana:
@MockBean– w kontekście zostaje mock. Jeśli bean tego typu już istnieje, jest zastępowany. Wszystkie wywołania są rejestrowane (możemy je weryfikować przezverify()), ale logika oryginalna nie wykonuje się. Idealne, gdy chcemy w pełni kontrolować zachowanie zależności (np. zewnętrznego API).@SpyBean– w kontekście zostaje prawdziwy bean opakowany wMockito.spy(). Domyślnie wszystkie metody wykonują się normalnie. Można selektywnie zastubować pojedyncze metody (doReturn().when(spy).method()– ostrożnie zwhen().thenReturn(), które wywołuje metodę). Stosujemy, gdy 90% logiki ma działać realnie, ale chcemy podmienić jeden fragment (np. zegar, generator ID).
Po użyciu tych adnotacji kontekst jest brudny (@DirtiesContext w tle) – Spring tworzy osobny kontekst dla testu, co może spowolnić wykonanie, ale zapewnia izolację. Zastąpienie beana działa również dla beanów zdefiniowanych w autokonfiguracji.
Przykład kodu:
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean // PaymentClient w kontekście to teraz mock - nie woła zewnętrznego API
private PaymentClient paymentClient;
@SpyBean // PricingCalculator to prawdziwy bean, ale możemy podmienić wybrane metody
private PricingCalculator pricingCalculator;
@Test
void powinienZlozycZamowienieZRabatem() {
// 1) MOCK - definiujemy zachowanie od zera
when(paymentClient.charge(any(), anyDouble()))
.thenReturn(new PaymentResult("OK", "txn-123"));
// 2) SPY - prawdziwa logika z podmianą jednej metody
// doReturn() używamy, by NIE wywoływać oryginalnej metody przy stubowaniu
doReturn(BigDecimal.valueOf(10)).when(pricingCalculator).getDiscountPercent("VIP");
Order order = orderService.placeOrder("VIP", 100.0);
assertThat(order.getStatus()).isEqualTo("PAID");
verify(paymentClient).charge(any(), eq(90.0)); // weryfikacja wywołania mocka
verify(pricingCalculator).calculateTotal(anyDouble(), any()); // metoda spy wykonała się realnie
}
}
Materiały
↑ Powrót na góręWydajność i równoległość
Jak diagnozować wolne testy i optymalizować suitę testów?
Odpowiedź w 30 sekund:
Zacznij od pomiaru: użyj raportu Surefire/Failsafe (target/surefire-reports) lub adnotacji @Timeout, by zidentyfikować klasy przekraczające próg. Najczęstsi winowajcy to niepotrzebne @SpringBootTest zamiast slice'ów (@WebMvcTest, @DataJpaTest), brak izolacji bazy (pełny restart kontekstu) oraz testy I/O bez mockowania. Włącz równoległość i przesuń wolne testy do osobnej grupy uruchamianej w CI.
Odpowiedź w 2 minuty:
Optymalizacja suite testów to klasyczny problem inżynierski: nie da się przyspieszyć tego, czego się nie zmierzyło. Pierwszym krokiem jest analiza raportu z Maven Surefire — pliki TEST-*.xml zawierają atrybut time na poziomie klasy i metody. Skrypt sortujący po time w 5 minut wskaże top 10 winowajców. Alternatywnie IntelliJ IDEA pokazuje czasy w drzewie testów, a Gradle generuje raport HTML w build/reports/tests/test/index.html. Adnotacja @Timeout (klasowa lub metodowa) wymusza failowanie testów przekraczających próg — można ją zastosować retroaktywnie i obserwować, które padają.
Najczęstsze źródła powolności w testach JVM-owych: (1) nadużycie @SpringBootTest — pełen kontekst potrafi startować 10-30 sekund, podczas gdy slice'y @WebMvcTest, @DataJpaTest, @JsonTest startują w sekundę. (2) Brak współdzielenia kontekstu — gdy każda klasa ma inną kombinację @MockBean, Spring tworzy nowy kontekst. Konsolidacja przez bazową klasę testową i @MockitoBean (lub @Replace) drastycznie pomaga. (3) Realne I/O — sieć, system plików, baza w Dockerze. Mockuj klient HTTP (MockWebServer), używaj H2 in-memory albo Testcontainers reused (testcontainers.reuse.enable=true). (4) Duże fixtures — wczytywanie 50 MB JSON-a na każdy test. Cachuj w @BeforeAll na statycznym polu. (5) Sleepy zamiast Awaitility — Thread.sleep(5000) zawsze czeka 5 sekund, Awaitility.await().atMost(5, SECONDS).until(...) kończy się gdy tylko warunek jest spełniony.
Po identyfikacji wolnych klas: oznacz je @Tag("slow") i uruchamiaj selektywnie. W CI definiujesz dwa joby: fast (-DexcludedGroups=slow) odpalany na każdy push i slow (-Dgroups=slow) odpalany przed merge. Maven Surefire wspiera -Dsurefire.timeout= jako globalny limit, a właściwy @Timeout jako kontrakt na poziomie testu. Włączenie równoległości (parallel.enabled=true) z reguły daje 3-5x przyspieszenie. Końcowy etap to monitorowanie regresji — utrzymuj próg czasu CI (np. budujesz dashboard z time z surefire-reports i alarmujesz gdy suite urośnie o 20%).
flowchart TD
A[Raport surefire-reports<br/>TEST-*.xml] --> B{Czas klasy > prog?}
B -- nie --> Z[OK]
B -- tak --> C[Analiza klasy]
C --> D{Spring Boot Test?}
D -- tak --> E[Zamien na slice<br/>@WebMvcTest / @DataJpaTest]
D -- nie --> F{Robi realne I/O?}
F -- siec --> G[Mockuj HTTP<br/>MockWebServer / WireMock]
F -- DB --> H[H2 in-memory<br/>lub Testcontainers reused]
F -- pliki --> I[Cache w @BeforeAll<br/>tempdir]
F -- nie --> J{Duze fixtures?}
J -- tak --> K[Lazy loading<br/>statyczne pole]
J -- nie --> L{Thread.sleep w kodzie?}
L -- tak --> M[Zamien na Awaitility<br/>.atMost timeouts]
L -- nie --> N{Brak izolacji?}
N -- wspolna DB --> O[@DirtiesContext lub<br/>@Transactional rollback]
N -- nie --> P[@Tag slow + osobny job CI]
E --> P
G --> P
H --> P
I --> P
K --> P
M --> P
O --> P
Przykład kodu:
# junit-platform.properties - timeouty globalne i tryb diagnozy
junit.jupiter.execution.timeout.default = 5s
junit.jupiter.execution.timeout.testable.method.default = 3s
junit.jupiter.execution.timeout.lifecycle.method.default = 10s
# Wlaczenie rownoleglosci do diagnozy
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.classes.default = concurrent
// Tagowanie wolnych testow
@Tag("slow")
@Execution(ExecutionMode.SAME_THREAD)
@SpringBootTest
class FullStackOrderFlowTest {
@Test
@Timeout(value = 30, unit = TimeUnit.SECONDS) // kontrakt SLA
void pelnyPrzeplywZamowienia() { /* ... */ }
}
// Awaitility zamiast Thread.sleep - konczy sie od razu po spelnieniu warunku
class AsyncProcessorTest {
@Test
void przetwarzaAsynchronicznie() {
processor.submit("zadanie");
// BLAD: zawsze czeka 5s, niezaleznie od tego, czy gotowe
// Thread.sleep(5000);
// OK: konczy sie max po 5s, ale zwykle po 200ms
await().atMost(5, SECONDS)
.pollInterval(100, MILLISECONDS)
.until(() -> processor.isDone("zadanie"));
}
}
// Cache duzych fixtures w statycznym polu - jeden raz na cala klase
class LargeFixtureTest {
private static byte[] CACHED_FIXTURE;
@BeforeAll
static void loadOnce() throws IOException {
CACHED_FIXTURE = Files.readAllBytes(Path.of("src/test/resources/big-50mb.bin"));
}
@Test void test1() { /* uzywa CACHED_FIXTURE */ }
@Test void test2() { /* uzywa CACHED_FIXTURE */ }
}
# Uruchamianie tylko wolnych testow lokalnie
mvn test -Dgroups=slow
# Szybkie testy w pre-commit (bez slow)
mvn test -DexcludedGroups=slow
# Globalny timeout dla Surefire (zabija test po 60s)
mvn test -Dsurefire.timeout=60
# Diagnoza najwolniejszych klas - posortuj raporty po czasie
grep -oE 'time="[0-9.]+".*name="[^"]+"' target/surefire-reports/TEST-*.xml \
| sort -t'"' -k2 -n -r | head -20
Materiały
- JUnit 5 - @Timeout annotation
- Awaitility - asynchronous testing
- Maven Surefire - forking and parallel execution
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
↑ Powrót na górę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
↑ Powrót na góręTesty zagnieżdżone i ordering
Czym jest @Nested i jakie korzyści daje zagnieżdżanie klas testowych?
Odpowiedź w 30 sekund:
@Nested to adnotacja JUnit 5 pozwalająca tworzyć zagnieżdżone (wewnętrzne, niestatyczne) klasy testowe wewnątrz klasy nadrzędnej. Dzięki temu można logicznie pogrupować testy według scenariuszy lub stanu obiektu (np. "gdy konto puste", "gdy konto z saldem"), uzyskując czytelną, hierarchiczną strukturę testów i lepszą organizację @BeforeEach/@AfterEach per kontekst.
Odpowiedź w 2 minuty:
@Nested umożliwia hierarchiczne organizowanie testów w grupy odpowiadające różnym stanom lub scenariuszom systemu pod testem (SUT). Każda zagnieżdżona klasa może mieć własne metody cyklu życia (@BeforeEach, @AfterEach), co eliminuje powtarzający się kod inicjalizacji i pozwala odzwierciedlić strukturę "given-when-then" bezpośrednio w organizacji kodu testowego. Wynikiem jest drzewiasta struktura widoczna w raporcie testów, np. Kalkulator > Gdy dochód poniżej 10k > zwraca zero podatku.
Kluczowa zasada: klasy zagnieżdżone muszą być niestatyczne (inner classes). Powodem jest dostęp do stanu klasy zewnętrznej — instancja klasy wewnętrznej zawsze posiada referencję do instancji klasy zewnętrznej. Dzięki temu testy zagnieżdżone mogą współdzielić pola, mocki i obiekty przygotowane w klasie nadrzędnej. Próba użycia static class z @Nested spowoduje błąd w czasie wykonania.
Korzyści: czytelniejszy raport testów (drzewo zamiast płaskiej listy), eliminacja duplikacji w setupie, możliwość grupowania asercji według wspólnego stanu, oraz wymuszenie strukturalnego myślenia o scenariuszach. Wadą jest większa złożoność dla prostych przypadków — dla 2-3 testów bez wspólnego kontekstu @Nested to overkill.
Przykład kodu:
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Kalkulator podatku PIT")
class TaxCalculatorTest {
private TaxCalculator calculator;
@BeforeEach
void setUp() {
calculator = new TaxCalculator();
}
@Nested
@DisplayName("Gdy dochód poniżej 10 000 zł")
class WhenIncomeBelow10k {
private final BigDecimal income = new BigDecimal("8000");
@Test
@DisplayName("zwraca zero podatku (kwota wolna)")
void zwracaZeroPodatku() {
// Korzystamy z calculator z klasy zewnętrznej
assertEquals(BigDecimal.ZERO, calculator.calculate(income));
}
@Nested
@DisplayName("Gdy zastosowano ulgę na dziecko")
class WhenSpecialDeduction {
@Test
@DisplayName("dalej zwraca zero")
void dalejZeroPodatku() {
calculator.applyChildDeduction();
assertEquals(BigDecimal.ZERO, calculator.calculate(income));
}
}
}
@Nested
@DisplayName("Gdy dochód powyżej progu 120 000 zł")
class WhenIncomeAboveThreshold {
@Test
@DisplayName("stosuje stawkę 32%")
void stosujeWyzszaStawke() {
BigDecimal income = new BigDecimal("150000");
assertTrue(calculator.calculate(income).compareTo(new BigDecimal("9600")) > 0);
}
}
}
Diagram struktury zagnieżdżonej:
graph TD
A[TaxCalculatorTest] --> B["@Nested WhenIncomeBelow10k"]
A --> C["@Nested WhenIncomeAboveThreshold"]
B --> D[zwracaZeroPodatku]
B --> E["@Nested WhenSpecialDeduction"]
E --> F[dalejZeroPodatku]
C --> G[stosujeWyzszaStawke]
style A fill:#4a90e2,color:#fff
style B fill:#7cb342,color:#fff
style C fill:#7cb342,color:#fff
style E fill:#fbc02d
Materiały
↑ Powrót na góręIntegracja z Mockito
Jak skonfigurować Mockito z JUnit 5 (@ExtendWith(MockitoExtension.class))?
Odpowiedź w 30 sekund:
Dodaj zależność mockito-junit-jupiter i oznacz klasę testową adnotacją @ExtendWith(MockitoExtension.class). Rozszerzenie automatycznie inicjalizuje pola oznaczone @Mock, @Spy, @Captor oraz @InjectMocks przed każdym testem, a po teście weryfikuje brak nieużytych stubów (strict stubs).
Odpowiedź w 2 minuty:
W JUnit 5 Mockito integruje się przez własne rozszerzenie MockitoExtension, dostarczane w artefakcie mockito-junit-jupiter. Wcześniej (JUnit 4) używało się MockitoJUnitRunner lub MockitoAnnotations.openMocks(this) w @BeforeEach. W JUnit 5 wystarczy dodać @ExtendWith(MockitoExtension.class) na poziomie klasy.
Rozszerzenie odpowiada za trzy rzeczy: (1) inicjalizację mocków oznaczonych adnotacjami przed każdym testem, (2) weryfikację po teście, że wszystkie stuby zostały użyte (domyślnie tryb STRICT_STUBS), (3) sprzątanie zasobów (zamykanie sesji Mockito). Tryb stricte można zmienić adnotacją @MockitoSettings(strictness = Strictness.LENIENT).
Alternatywą jest ręczna inicjalizacja przez MockitoAnnotations.openMocks(this) w metodzie @BeforeEach — przydatna, gdy nie chcemy uzależniać testu od rozszerzenia (np. w testach łączących wiele extensionów). Mockito od wersji 3 wymaga JDK 8+ i automatycznie korzysta z mockito-inline lub klasycznego mockito-core w zależności od potrzeby mockowania metod static/final.
Przykład kodu:
// build.gradle (Kotlin DSL)
// testImplementation("org.mockito:mockito-junit-jupiter:5.11.0")
// testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
// pom.xml
// <dependency>
// <groupId>org.mockito</groupId>
// <artifactId>mockito-junit-jupiter</artifactId>
// <version>5.11.0</version>
// <scope>test</scope>
// </dependency>
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
// Rozszerzenie inicjalizuje mocki i weryfikuje stuby
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS) // domyślne, można zmienić na LENIENT
class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway; // mock — wszystkie metody zwracają wartości domyślne
@InjectMocks
private OrderService orderService; // SUT — Mockito wstrzykuje paymentGateway
@Test
void powinienOplacicZamowienie() {
// given — definicja zachowania mocka
when(paymentGateway.charge(100)).thenReturn(true);
// when
boolean wynik = orderService.placeOrder(100);
// then
assertThat(wynik).isTrue();
}
}
// Alternatywa bez rozszerzenia — ręczna inicjalizacja
class AlternatywnaKonfiguracjaTest {
@Mock private PaymentGateway gateway;
private AutoCloseable mocks;
@BeforeEach
void setUp() {
mocks = MockitoAnnotations.openMocks(this); // ręczna inicjalizacja
}
@AfterEach
void tearDown() throws Exception {
mocks.close(); // ważne — uwalnia zasoby Mockito
}
}
Materiały
↑ Powrót na góręWzorce i strategie testowe
Jak stosować zasadę AAA (Arrange-Act-Assert) w testach JUnit 5?
Odpowiedź w 30 sekund: Zasada AAA dzieli test na trzy wyraźne sekcje: Arrange (przygotowanie danych i mocków), Act (wywołanie testowanej metody) oraz Assert (weryfikacja rezultatu). Taki podział sprawia, że testy są czytelne, łatwe w utrzymaniu i jednoznacznie pokazują intencję autora.
Odpowiedź w 2 minuty: Wzorzec AAA (Arrange-Act-Assert), nazywany również Given-When-Then w stylu BDD, to fundamentalna struktura testu jednostkowego. Sekcja Arrange odpowiada za przygotowanie kontekstu — tworzenie obiektów, ustawianie wartości, konfigurację mocków oraz definiowanie danych wejściowych. Powinna zawierać wszystko, czego test potrzebuje, ale nic ponadto.
Sekcja Act to pojedyncze wywołanie testowanej metody — to ten jeden moment, w którym wykonujemy operację, którą chcemy sprawdzić. Powinna być możliwie krótka, najlepiej jednolinijkowa, co ułatwia czytanie testu i znalezienie sedna testowanego zachowania.
Sekcja Assert weryfikuje, czy wynik odpowiada oczekiwaniom. Powinna zawierać asercje skupione na pojedynczym, logicznym aspekcie zachowania. Używaj AssertJ (assertThat(...).isEqualTo(...)) zamiast surowych asercji JUnit, bo daje bogatsze komunikaty błędów i fluent API.
Dobrą praktyką jest oddzielanie sekcji pustą linią lub komentarzami // Arrange, // Act, // Assert. Trzymanie się tej struktury wymusza pisanie testów, które robią tylko jedną rzecz, co ułatwia diagnozę przy regresji — od razu wiadomo, które zachowanie przestało działać.
flowchart LR
A["Arrange<br/>Setup danych<br/>i mocków"] --> B["Act<br/>Wywołanie<br/>testowanej metody"]
B --> C["Assert<br/>Weryfikacja<br/>rezultatu"]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#e8f5e9
Przykład kodu:
@Test
void powinienObliczycCeneCalkowita_gdyKoszykZawieraProdukty() {
// Arrange - przygotowanie danych testowych
Cart cart = new Cart();
cart.addItem(new Product("Książka", new BigDecimal("29.99")), 2);
cart.addItem(new Product("Długopis", new BigDecimal("5.50")), 3);
PricingService pricingService = new PricingService();
// Act - wywołanie testowanej operacji
BigDecimal totalPrice = pricingService.calculateTotal(cart);
// Assert - weryfikacja oczekiwanego wyniku
assertThat(totalPrice)
.isEqualByComparingTo(new BigDecimal("76.48"));
}
// Antywzorzec: brak wyraźnego podziału, wiele asercji bez logicznego ciągu
@Test
void zlyTest() {
Cart cart = new Cart();
cart.addItem(new Product("X", BigDecimal.TEN), 1);
assertThat(cart.getItems()).hasSize(1); // assert za wcześnie
cart.addItem(new Product("Y", BigDecimal.ONE), 2); // kolejny act
assertThat(cart.getTotal()).isEqualByComparingTo("12"); // mieszane sekcje
}
Materiały
↑ Powrót na góręMigracja i najlepsze praktyki
Jakie są największe pułapki migracji z JUnit 4 do JUnit 5?
Odpowiedź w 30 sekund:
Najczęstsze pułapki to zmiana pakietów (org.junit → org.junit.jupiter.api), inna składnia adnotacji (@Before → @BeforeEach, @BeforeClass → @BeforeAll), brak parametru expected w @Test (teraz assertThrows), zastąpienie @RunWith przez @ExtendWith oraz brak Hamcresta w klasycznym zestawie asercji. Dodatkowo metody @BeforeAll/@AfterAll muszą być statyczne (chyba że klasa ma @TestInstance(PER_CLASS)).
Odpowiedź w 2 minuty:
Migracja z JUnit 4 do JUnit 5 wygląda na prostą zamianę adnotacji, ale w praktyce kryje wiele pułapek. Pierwsza i najbardziej oczywista to zmiana pakietu importów: cały kod testowy musi przejść z org.junit.* na org.junit.jupiter.api.*. Sama adnotacja @Test została przeniesiona i co ważniejsze, nie przyjmuje już parametrów expected ani timeout — trzeba użyć assertThrows() oraz assertTimeout()/assertTimeoutPreemptively().
Druga pułapka to cykl życia. @Before zmienia się w @BeforeEach, @After w @AfterEach, a @BeforeClass/@AfterClass w @BeforeAll/@AfterAll. Te ostatnie nadal muszą być statyczne, chyba że oznaczymy klasę @TestInstance(Lifecycle.PER_CLASS). @Ignore zostało zastąpione przez @Disabled, a @Category przez @Tag. Runnery JUnit 4 (@RunWith(SpringRunner.class), MockitoJUnitRunner) trzeba przepisać na rozszerzenia (@ExtendWith(SpringExtension.class), @ExtendWith(MockitoExtension.class)).
Trzecia pułapka dotyczy asercji i bibliotek pomocniczych. JUnit 5 nie zawiera już Hamcresta w transitive dependencies — jeśli używasz assertThat(value, is(...)), musisz dodać hamcrest jawnie albo przejść na AssertJ. Reguły JUnit 4 (@Rule, TemporaryFolder, ExpectedException) nie działają w Jupiter — trzeba je zastąpić rozszerzeniami albo wbudowanymi mechanizmami (@TempDir, assertThrows). Wreszcie testy parametryzowane mają zupełnie inną składnię: zamiast @RunWith(Parameterized.class) używamy @ParameterizedTest z @ValueSource, @CsvSource lub @MethodSource.
flowchart LR
subgraph JUnit4["JUnit 4"]
A1["@Before"]
A2["@BeforeClass"]
A3["@After"]
A4["@AfterClass"]
A5["@Ignore"]
A6["@Category"]
A7["@Test(expected=Ex.class)"]
A8["@Test(timeout=1000)"]
A9["@RunWith(X.class)"]
A10["@Rule TemporaryFolder"]
A11["@RunWith(Parameterized)"]
end
subgraph JUnit5["JUnit 5 (Jupiter)"]
B1["@BeforeEach"]
B2["@BeforeAll (static)"]
B3["@AfterEach"]
B4["@AfterAll (static)"]
B5["@Disabled"]
B6["@Tag"]
B7["assertThrows(Ex.class, ...)"]
B8["assertTimeout(...)"]
B9["@ExtendWith(X.class)"]
B10["@TempDir Path tmp"]
B11["@ParameterizedTest + @MethodSource"]
end
A1 --> B1
A2 --> B2
A3 --> B3
A4 --> B4
A5 --> B5
A6 --> B6
A7 --> B7
A8 --> B8
A9 --> B9
A10 --> B10
A11 --> B11
Przykład kodu:
// PRZED: JUnit 4
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@BeforeClass
public static void initAll() { /* setup raz */ }
@Before
public void init() { /* setup per test */ }
@Test(expected = IllegalArgumentException.class)
public void shouldFailForNull() {
new User(null);
}
@Ignore("WIP")
@Test
public void skipped() { }
}
// PO: JUnit 5 (Jupiter)
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@BeforeAll
static void initAll() { /* setup raz - musi być static */ }
@BeforeEach
void init() { /* setup per test */ }
@Test
void shouldFailForNull() {
// assertThrows zamiast expected=
assertThrows(IllegalArgumentException.class, () -> new User(null));
}
@Disabled("WIP")
@Test
void skipped() { }
}
Materiały
↑ Powrót na górę