JUnit 5 Advanced Features

JUnit 5 (Jupiter) replaced JUnit 4 in the late 2010s. Beyond the basic test-method-with-`@Test`-annotation pattern, it added features that change how more complex tests are written: parameterized tests, nested classes, dynamic tests, and a richer extension model. This page covers the features that pay off in practice.

Parameterized tests

The same test logic with different inputs:

```java

@ParameterizedTest

@ValueSource(strings = {"alice", "bob", "carol"})

void shouldAcceptValidUsername(String username) {

assertTrue(validator.isValid(username));

}

```

Multiple sources:

| Annotation | Use |

|------------|-----|

| `@ValueSource` | Single primitive or String value |

| `@CsvSource` | Multiple values per test, inline |

| `@CsvFileSource` | CSV file as source |

| `@MethodSource` | Method-provided arguments |

| `@EnumSource` | Enum values |

| `@ArgumentsSource` | Custom argument provider |

```java

@ParameterizedTest

@CsvSource({

"[email protected], true",

"invalid, false",

"', false"

})

void shouldValidateEmails(String input, boolean expected) {

assertEquals(expected, EmailValidator.isValid(input));

}

```

Reduces test duplication and makes coverage explicit.

Nested test classes

`@Nested` for hierarchical organization:

```java

class OrderTest {

@Nested

class WhenOrderIsPending {

@Test

void canBeCancelled() { /* ... */ }

@Test

void cannotBeShipped() { /* ... */ }

}

@Nested

class WhenOrderIsConfirmed {

@Test

void cannotBeCancelled() { /* ... */ }

@Test

void canBeShipped() { /* ... */ }

}

}

```

Useful for representing state-dependent behavior. Each `@Nested` class can have its own setup; tests within share that setup.

Dynamic tests

Tests generated at runtime:

```java

@TestFactory

Stream<DynamicTest> shouldHandleAllSupportedFormats() {

return Stream.of("json", "xml", "yaml")

.map(format -> DynamicTest.dynamicTest(

"should parse " + format,

() -> assertNotNull(parser.parse(format, sampleInput))));

}

```

Useful when test cases depend on runtime data (file system contents, database state, configuration).

Test lifecycle

The standard hooks:

- `@BeforeAll`: once before all tests in the class (must be static unless `@TestInstance(Lifecycle.PER_CLASS)`)

- `@BeforeEach`: before each test method

- `@AfterEach`: after each test method

- `@AfterAll`: once after all tests in the class

`@TestInstance(Lifecycle.PER_CLASS)` reuses the same test instance across all methods, allowing non-static `@BeforeAll`. Useful for expensive setup.

Assertions

JUnit 5 assertions:

```java

assertEquals(expected, actual);

assertTrue(condition);

assertNotNull(obj);

assertThrows(IllegalArgumentException.class, () -> service.process(invalid));

assertTimeout(Duration.ofSeconds(1), () -> longRunning());

// Multiple assertions; runs all even if some fail

assertAll(

() -> assertEquals("abc", result.id()),

() -> assertTrue(result.amount() > 0),

() -> assertEquals(OrderStatus.PENDING, result.status())

);

```

For more readable assertions, AssertJ's fluent API is widely preferred:

```java

assertThat(result)

.isNotNull()

.extracting(Order::id, Order::amount, Order::status)

.contains("abc", 99.0, OrderStatus.PENDING);

```

Extensions

JUnit 5's `@ExtendWith` mechanism replaces JUnit 4 runners and rules. Extensions hook into the test lifecycle to provide framework integration.

Common extensions

- `@ExtendWith(MockitoExtension.class)` — Mockito support

- `@ExtendWith(SpringExtension.class)` — Spring tests (often via `@SpringBootTest`)

- `@ExtendWith(MyExtension.class)` — custom extensions

Custom extensions

For project-specific test infrastructure:

```java

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

@Override

public void beforeTestExecution(ExtensionContext context) {

context.getStore(NAMESPACE).put("startTime", System.nanoTime());

}

@Override

public void afterTestExecution(ExtensionContext context) {

long duration = System.nanoTime() - (long) context.getStore(NAMESPACE).get("startTime");

System.out.println(context.getDisplayName() + " took " + duration + "ns");

}

}

```

Extensions earn their place when test infrastructure is shared across many tests.

Display names

```java

@Test

@DisplayName("should reject invalid email formats")

void shouldRejectInvalid() { /* ... */ }

```

The display name appears in test reports. For tests with names that read as sentences, this is helpful.

For parameterized tests, the parameter values are in the display name automatically.

Tagging

```java

@Test

@Tag("slow")

void integrationTest() { /* ... */ }

```

Useful with build tools to run subsets:

```bash

mvn test -Dgroups=fast

mvn test -Dgroups=integration

```

Maintains the unit/integration distinction without separate source directories.

Conditional execution

Skip tests based on conditions:

```java

@Test

@EnabledOnOs(OS.LINUX)

void linuxOnlyTest() { /* ... */ }

@Test

@EnabledIfEnvironmentVariable(named = "CI", matches = "true")

void ciOnlyTest() { /* ... */ }

@Test

@DisabledIf("isLegacyMode")

void modernOnlyTest() { /* ... */ }

```

Common failure patterns

- **One giant test class with hundreds of tests.** Hard to navigate; nest or split.

- **Mutable state shared across tests.** `@BeforeEach` should reset; otherwise tests interfere.

- **Sleep-based timing in tests.** Flaky; use `assertTimeout` or proper synchronization.

- **Mocking everything.** Tests pass while production fails. Test against real implementations where reasonable.

- **Skipping tests with `@Disabled` and forgetting them.** Run periodically; remove or fix.

Further Reading

- [JavaTwentyOneFeatures](JavaTwentyOneFeatures) — Modern features used in test code

- [JavaCollectionsFramework](JavaCollectionsFramework) — Test data structures

- [DebuggingStrategies](DebuggingStrategies) — Tests as debugging aid

- [SpringBootFundamentals](SpringBootFundamentals) — Spring's test slices

- [Java Hub](JavaHub) — Cluster index