Skip to content
End To End Tester

JUnit 5

The Java standard — Jupiter's extension model, parameterised tests, nested classes, assertions with AssertJ, and running it all in CI.

1 min read · updated 19 September 2026

JUnit 5 — Jupiter — is the Java testing standard, and its extension model is the most capable of any framework covered here.

#The shape

java
class BasketTest {

    Basket basket;

    @BeforeEach void setUp()    { basket = new Basket(); }
    @AfterEach  void tearDown() { }

    @BeforeAll  static void onceBefore() { }   // static unless @TestInstance(PER_CLASS)

    @Test
    @DisplayName("an empty basket costs nothing")
    void emptyBasketCostsNothing() {
        assertThat(basket.totalCents()).isZero();
    }

    // Nested classes group related cases and share setup, which keeps
    // long test files navigable.
    @Nested
    @DisplayName("when it holds two books")
    class WithTwoBooks {
        @BeforeEach void addBooks() { basket.add("book-1", 2, 1_200); }

        @Test void totalsTheLines()       { assertThat(basket.totalCents()).isEqualTo(2_400); }
        @Test void countsTheItems()       { assertThat(basket.itemCount()).isEqualTo(2); }
    }
}

#Parameterised tests

java
@ParameterizedTest(name = "an order of {0} is discounted by {1}")
@CsvSource({ "12000, 2400", "10000, 2000", "9999, 0" })
void discountAppliesAtOrAboveTheThreshold(int subtotal, int expected) {
    assertThat(Pricing.applyDiscount(new Order(subtotal), POLICY).discountCents())
        .isEqualTo(expected);
}

@ParameterizedTest
@EnumSource(CustomerTier.class)
void everyTierHasAShippingPrice(CustomerTier tier) {
    assertThat(Shipping.forTier(tier).cents()).isNotNegative();
}

@ParameterizedTest
@MethodSource("shippingCases")
void shippingCases(CustomerTier tier, int expected) { /* ... */ }

static Stream<Arguments> shippingCases() {
    return Stream.of(
        arguments(CustomerTier.STANDARD, 395),
        arguments(CustomerTier.GOLD, 0));
}

// Every combination of two sources — @CartesianTest, from junit-pioneer
@CartesianTest
void shippingForEveryTierAndRegion(
        @CartesianTest.Enum CustomerTier tier,
        @CartesianTest.Values(strings = {"UK", "EU"}) String region) { }

#AssertJ

JUnit's own assertions stop being enough quickly. AssertJ is effectively standard:

java
assertThat(order.lines())
    .hasSize(2)
    .extracting(OrderLine::sku)
    .containsExactlyInAnyOrder("book-1", "pen-2");

assertThat(order)
    .extracting(Order::status, Order::totalCents)
    .containsExactly(OrderStatus.PAID, 2_795);

assertThatThrownBy(() -> service.reserve(order))
    .isInstanceOf(InsufficientStockException.class)
    .hasMessageContaining("book-1");

// Soft assertions: report every failure, not just the first
SoftAssertions.assertSoftly(softly -> {
    softly.assertThat(order.status()).isEqualTo(PAID);
    softly.assertThat(order.totalCents()).isEqualTo(2_795);
});

#Extensions

The extension model replaces JUnit 4's runners and rules, and composes — you can have as many as you like.

java
// A reusable extension: freeze the clock for any test that asks.
public class FixedClockExtension implements BeforeEachCallback, AfterEachCallback {
    @Override public void beforeEach(ExtensionContext context) {
        ClockHolder.set(Clock.fixed(Instant.parse("2026-01-01T12:00:00Z"), ZoneOffset.UTC));
    }
    @Override public void afterEach(ExtensionContext context) {
        ClockHolder.reset();
    }
}

@ExtendWith(FixedClockExtension.class)
@ExtendWith(MockitoExtension.class)
class TokenTest {
    @Mock TokenStore store;

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

Testcontainers ships @Testcontainers, Spring ships @SpringBootTest, Mockito ships MockitoExtension — all the same mechanism.

#Parallelism

properties
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4

Classes concurrent, methods within a class sequential, is the safe default. Opt individual classes up with @Execution(CONCURRENT) and down with @Execution(SAME_THREAD) or @ResourceLock.

java
// Declare a shared resource and JUnit serialises access to it.
@ResourceLock(value = "database", mode = READ_WRITE)
class MigrationTest { }

@ResourceLock is a genuinely nice feature that most frameworks lack — it lets a suite be mostly parallel with declared exceptions. See parallel test execution.

#Tags and filtering

java
@Tag("integration")
class OrderRepositoryTest { }
bash
./gradlew test --tests '*' -PexcludeTags=integration
mvn test -Dgroups='!integration'

Splitting fast and slow suites by tag is how you keep the inner loop fast while still running everything in CI.

#What goes with it

Mockito for doubles (see mocking frameworks), Testcontainers for real dependencies, WireMock for HTTP stubs, and JaCoCo for coverage. That stack has been stable for years and is the default for a reason.

Common questions

What is the difference between JUnit 4 and JUnit 5?
JUnit 5 is a rewrite in three parts — Platform, Jupiter and Vintage. The user-visible changes are a new annotation set, a proper extension model replacing runners and rules, nested test classes, and far better parameterisation. Vintage runs JUnit 4 tests on the new platform, so migration can be gradual.
Should I use JUnit's built-in assertions or AssertJ?
AssertJ, for anything beyond assertEquals. Its failure messages are dramatically better on collections and objects, and the fluent API makes intent obvious. JUnit's own assertions are fine for simple equality.
How do I run JUnit 5 tests in parallel?
Enable it in junit-platform.properties and annotate classes with @Execution. Parallelism is off by default, and turning it on will surface any shared state the suite has been getting away with.

Runnable samples for this page

last test results ↗
  • Javajava/src/test/java/tools/junit

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?