JUnit 5 separates the API used to write tests from the infrastructure that discovers and executes them. The separation lets Maven, IDEs, and custom launchers run several test models through one interface.
The examples target JUnit 5.13.4. JUnit 6 retains the same architecture, but a project that must remain on JUnit 5 should use the versioned JUnit 5 documentation.
Platform, Jupiter, and Vintage #
JUnit 5 consists of three components:
| Component | Responsibility |
|---|---|
| JUnit Platform | Discovers tests and launches registered test engines. Maven Surefire and IDEs integrate at this level. |
| JUnit Jupiter | Defines the JUnit 5 programming model, extension API, and test engine. |
| JUnit Vintage | Runs JUnit 3 and JUnit 4 tests on the Platform during migration. |
A Jupiter test imports @Test and assertions from org.junit.jupiter.api.
Surefire asks the Platform to discover tests, then
junit-jupiter-engine executes the discovered methods. Application code does
not depend on any JUnit component because test dependencies use Maven’s
test scope.
Minimal Maven Project #
The following layout keeps production and test classes separate:
junit-hello/
├── pom.xml
└── src
├── main/java/com/example/Greeter.java
└── test/java/com/example/GreeterTest.javaUse org.junit.jupiter:junit-jupiter for the smallest maintainable POM. The
artifact aggregates the Jupiter API, parameterized-test support, and runtime
engine.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>junit-hello</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<junit.version>5.13.4</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>package com.example;
public final class Greeter {
public String greet(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name must not be blank");
}
return "Hello, " + name;
}
}package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class GreeterTest {
private final Greeter greeter = new Greeter();
@Test
void greetsByName() {
assertEquals("Hello, Ada", greeter.greet("Ada"));
}
@Test
void rejectsBlankNames() {
var error = assertThrows(
IllegalArgumentException.class,
() -> greeter.greet(" "));
assertEquals("name must not be blank", error.getMessage());
}
}Run the test through Maven:
mvn testJupiter test classes and methods may have package-private visibility. Test
methods must not be private or static, although lifecycle and extension
rules permit static in specific places.
Choosing JUnit Artifacts #
Add another artifact only when its capability is required:
| Artifact | When to Add It |
|---|---|
junit-jupiter | Prefer the aggregate dependency for an application test suite. It already includes API, parameters, and engine. |
junit-jupiter-api | Compile tests or extensions against the API while managing runtime engines separately. |
junit-jupiter-engine | Execute Jupiter tests when using fine-grained dependencies instead of junit-jupiter. Use test runtime scope. |
junit-jupiter-params | Add @ParameterizedTest support to a fine-grained setup. The aggregate dependency already includes it. |
junit-vintage-engine | Run unchanged JUnit 3 or JUnit 4 tests through the Platform. |
junit-jupiter-migrationsupport | Temporarily adapt selected JUnit 4 rules and @Ignore while converting tests to Jupiter. |
junit-platform-suite | Declare suites with Platform suite annotations. |
junit-platform-launcher | Discover and execute tests programmatically, or implement test tooling. Application test suites do not need it directly. |
junit-platform-console-standalone | Run tests outside Maven or an IDE from one executable JAR. Keep the JAR outside the project POM. |
Import the JUnit BOM when a build declares several JUnit artifacts because the BOM keeps Platform, Jupiter, and Vintage versions compatible:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Artifact declarations covered by the BOM can omit <version>.
An explicit junit-platform-launcher dependency was necessary with Surefire
releases before 3.0. Current Surefire 3.x releases align the launcher with the
Platform on the test runtime classpath, so adding a launcher to an ordinary
test suite creates another version to manage without adding test capability.
Jupiter Programming Model #
Jupiter provides focused annotations for common test structures:
@BeforeEachand@AfterEachestablish per-test fixtures.@BeforeAlland@AfterAllestablish class-level fixtures.@ParameterizedTestexecutes one test body with data from@ValueSource,@CsvSource,@MethodSource, or another argument source.@Nestedgroups related cases while preserving lifecycle semantics.@Tagclassifies tests for build-time inclusion or exclusion.@ExtendWithregisters extensions for parameter resolution, callbacks, conditional execution, and other cross-cutting behavior.
Tests should assert observable results rather than execution order. Jupiter does not guarantee a self-evident order from method names, and ordered tests share state that makes failures dependent on earlier cases.
Running JUnit 4 Tests Unchanged #
Vintage provides the lowest-risk first migration step because Jupiter and JUnit 4 use different Java packages and can coexist in one module:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>Surefire then discovers Jupiter and JUnit 4 tests through the Platform. Keep Vintage only while old tests remain because a successful Vintage run does not exercise Jupiter’s programming or extension model.
Converting JUnit 4 Tests #
Common source mappings are:
| JUnit 4 | Jupiter |
|---|---|
@Before / @After | @BeforeEach / @AfterEach |
@BeforeClass / @AfterClass | @BeforeAll / @AfterAll |
@Ignore | @Disabled |
@Category | @Tag |
@RunWith | One or more @ExtendWith extensions |
@Rule / @ClassRule | An extension or Jupiter-native fixture |
@Test(expected = E.class) | assertThrows(E.class, executable) |
@RunWith to @ExtendWith is a conceptual mapping rather than a mechanical
rename because a JUnit 4 class can have one runner while Jupiter composes
multiple extensions.
JUnit 4 places an optional assertion message first:
assertEquals("unexpected greeting", expected, actual);Jupiter places the message last:
assertEquals(expected, actual, "unexpected greeting");Existing org.junit.Assert calls can remain while annotations and lifecycle
code move to Jupiter. Converting assertions separately reduces the size of
each migration change.
Rules need individual treatment. The junit-jupiter-migrationsupport artifact
adapts ExternalResource, Verifier, and ExpectedException rule families;
@EnableRuleMigrationSupport activates those adapters.
@EnableJUnit4MigrationSupport also recognizes JUnit 4 @Ignore. Other
custom rules require a Jupiter extension or a direct fixture rewrite.
A controlled migration therefore follows three stages:
- Add Vintage and establish one Platform-based test command.
- Convert classes to Jupiter, replacing runners and rules according to their behavior rather than their names.
- Remove Vintage, JUnit 4, and migration support after the build contains no JUnit 3 or JUnit 4 tests.
Comments
Read or comment on GitHub (sign-in required to comment)