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
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
@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:
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.
// 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
# 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 = 4Classes 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.
// 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
@Tag("integration")
class OrderRepositoryTest { }./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 ↗- Java
java/src/test/java/tools/junit
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Writing Tests in JavaJUnit 5, Mockito, AssertJ and Testcontainers — the stack that has been stable for a decade, plus Spring's test slices and what they cost.
- Mocking FrameworksMoq, NSubstitute, Mockito, unittest.mock, Sinon and the rest — what each ecosystem's mocking library does well, and the failure modes they share.
- TestcontainersRunning real databases, brokers and services as disposable containers from inside your test suite — the pattern that made integration testing cheap.
- Running Tests in ParallelHow parallelism turns hidden coupling into failures, the shared resources that clash, and how to make a suite genuinely safe to run concurrently.
- WireMockA programmable HTTP server for tests — stubbing responses, injecting failures, verifying requests, and recording real traffic to replay.
- Android App TestingJUnit and Robolectric for local tests, Espresso and Compose for instrumented UI, and the emulator strategy that keeps an Android suite affordable in CI.
- Arrange-Act-AssertThe three-part shape every readable test has, why the act step should be one line, and the smells that show up when a test will not fit the pattern.