To use JUnit 5 in a Maven project, we import the junit-bom and add one test-scoped dependency, org.junit.jupiter:junit-jupiter, which brings the API for writing tests, the parameterized test support and the engine that runs them. Maven Surefire 3.x finds the engine on the test classpath and runs the tests during mvn test.
We need this setup in every new Maven project with tests, and when we move an old project from JUnit 4 to JUnit 5.
The following example is the smallest pom.xml part that runs JUnit 5 tests with the latest JUnit 5 release, 5.14.4, and Maven Surefire 3.6.0.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.14.4</version> <!-- aligns every JUnit artifact -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <!-- API + params + engine -->
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version> <!-- 3.x runs JUnit 5 without extra setup -->
</plugin>
</plugins>
</build>
[INFO] Running com.howtodoinjava.junit5maven.TemperatureConverterTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.290 s -- in com.howtodoinjava.junit5maven.TemperatureConverterTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Notice that only the junit-bom has a version. The BOM sets the versions of junit-jupiter and of every other JUnit artifact we add later, so they always match.
Next, we look at the JUnit 5 modules and the artifact for each job, a complete pom.xml with Failsafe, and the version problems that make mvn test run zero tests or fail. After that, we run JUnit 4 tests next to JUnit 5 tests, set the JUnit version in a Spring Boot project, and see when to move to JUnit 6.
1. JUnit 5 Modules and Maven Artifacts
JUnit 5 is not one jar. It consists of three sub-projects, and each one ships several Maven artifacts.
- JUnit Platform (group org.junit.platform, version 1.14.4) launches test frameworks on the JVM. Build tools and IDEs talk to the Platform, never to a test framework.
- JUnit Jupiter (group org.junit.jupiter, version 5.14.4) is the programming model for JUnit 5 tests, with annotations such as @Test, the Assertions class and the extension model, plus the TestEngine that runs Jupiter tests on the Platform.
- JUnit Vintage (group org.junit.vintage) is a TestEngine that runs JUnit 3 and JUnit 4 tests on the Platform.

Most projects need only the first row of the table. The other artifacts are for specific features, and with the BOM imported, we add them without a version.
| Maven artifact | What it contains | When we add it |
|---|---|---|
| org.junit.jupiter:junit-jupiter | aggregate of junit-jupiter-api, junit-jupiter-params and junit-jupiter-engine | always (recommended) |
| org.junit.jupiter:junit-jupiter-api | @Test, lifecycle annotations, Assertions, Assumptions | only when we list the modules one by one |
| org.junit.jupiter:junit-jupiter-params | @ParameterizedTest and the argument sources | included in junit-jupiter |
| org.junit.jupiter:junit-jupiter-engine | the TestEngine that runs Jupiter tests | included in junit-jupiter, needed at test runtime only |
| org.junit.platform:junit-platform-suite | @Suite, @SelectPackages and the suite engine | for test suites |
| org.junit.platform:junit-platform-launcher | the API that IDEs and build tools use to start tests | Surefire 3.x adds it itself; Gradle needs it as testRuntimeOnly |
| org.junit.vintage:junit-vintage-engine | the engine for JUnit 3 and JUnit 4 tests | while old JUnit 4 tests still exist |
| org.junit:junit-bom | no code, only version numbers | always, in dependencyManagement |
We use the aggregate junit-jupiter artifact instead of listing junit-jupiter-api and junit-jupiter-engine separately. The single dependency gives us parameterized tests as well, and there is no second version number to keep in sync.
2. A Complete JUnit 5 pom.xml
A typical project runs fast unit tests with Surefire in the test phase, and slower integration tests with the Maven Failsafe plugin in the integration-test phase. By default, Surefire picks the classes whose names start with Test or end with Test, Tests or TestCase, and Failsafe picks the classes whose names start with IT or end with IT or ITCase. JUnit 5 itself runs on Java 8 or later, and the example compiles for Java 25.
<?xml version="1.0" encoding="UTF-8"?>
<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.howtodoinjava</groupId>
<artifactId>junit5-maven</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.14.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
The unit test class TemperatureConverterTest has one @Test method and one parameterized test with three rows, so Surefire reports 4 tests. The integration test TemperatureConverterIT uses the same JUnit 5 annotations, and only its class name tells Maven to run it with Failsafe.
class TemperatureConverterTest {
private final TemperatureConverter converter = new TemperatureConverter();
@Test
void boilingPoint() {
assertEquals(212.0, converter.toFahrenheit(100));
}
@ParameterizedTest
@CsvSource({"0, 32", "37, 98.6", "-40, -40"})
void celsiusToFahrenheit(double celsius, double fahrenheit) {
assertEquals(fahrenheit, converter.toFahrenheit(celsius), 0.001);
}
}
We run both kinds of tests with mvn verify. Surefire runs the 4 unit tests first, and Failsafe runs the integration test after the package phase.
[INFO] --- surefire:3.6.0:test (default-test) @ junit5-maven ---
[INFO] Running com.howtodoinjava.junit5maven.TemperatureConverterTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- failsafe:3.6.0:integration-test (default) @ junit5-maven ---
[INFO] Running com.howtodoinjava.junit5maven.TemperatureConverterIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- failsafe:3.6.0:verify (default) @ junit5-maven ---
[INFO] BUILD SUCCESS
3. Version Problems in JUnit 5 Maven Builds
Most JUnit 5 setup problems in Maven come from two version mistakes. The Surefire plugin is too old, or the JUnit artifacts have different versions.
3.1. Old Surefire Version Runs Zero Tests
Surefire supports the JUnit Platform since version 2.22.0. An older Surefire does not know the Platform, so it finds no tests and still reports success. For example, with Surefire 2.12.4, a project with 4 JUnit 5 tests prints this.
[INFO] --- surefire:2.12.4:test (default-test) @ junit5-maven ---
T E S T S
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Nobody notices a green build with zero tests in a CI log. Maven 3.9.11 binds Surefire 3.2.5 when the pom.xml has no Surefire version, but Maven releases before 3.9.0 bind Surefire 2.12.4. So we always declare the Surefire version in the pom.xml. The old junit-platform-surefire-provider plugin dependency was only a workaround for Surefire versions before 2.22.0, and current builds don’t need it.
3.2. Mixed JUnit Versions Fail the Build
The Jupiter engine needs the Platform classes of its own release. When we list the artifacts one by one with different versions, e.g. junit-jupiter-api 5.14.4 and junit-jupiter-engine 5.9.1, the test JVM fails during startup.
[ERROR] java.lang.NoClassDefFoundError: org/junit/platform/engine/support/store/NamespacedHierarchicalStore
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
The same error shows up when another library or a parent POM pulls in an older JUnit artifact. The junit-bom prevents both cases, because Maven takes the version of every org.junit artifact from the BOM, including the transitive ones. The dependency:tree goal shows the versions Maven resolved, and in the broken build the old engine stands out.
[INFO] com.howtodoinjava:junit5-maven:jar:1.0.0
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.14.4:test
[INFO] +- org.junit.jupiter:junit-jupiter-params:jar:5.14.4:test
[INFO] \- org.junit.jupiter:junit-jupiter-engine:jar:5.9.1:test
4. Running JUnit 4 Tests With the Vintage Engine
A large project rarely migrates all its JUnit 4 tests at once. The JUnit Vintage engine runs the old org.junit.Test tests on the JUnit Platform, so Surefire runs JUnit 4 and JUnit 5 tests in the same build. We add JUnit 4.13.2 and the junit-vintage-engine, whose version comes from the BOM.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<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>
<scope>test</scope>
</dependency>
The class LegacyConverterTest uses org.junit.Test from JUnit 4, and ConverterTest uses org.junit.jupiter.api.Test from JUnit 5. Surefire runs both classes in one run.
[INFO] Running com.howtodoinjava.vintage.ConverterTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.215 s -- in com.howtodoinjava.vintage.ConverterTest
[INFO] Running com.howtodoinjava.vintage.LegacyConverterTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.056 s -- in com.howtodoinjava.vintage.LegacyConverterTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
We remove the two extra dependencies once the last JUnit 4 test is migrated. The JUnit 5 vs JUnit 4 comparison lists the annotations that change during the migration.
5. JUnit 5 in a Spring Boot Project
In a Spring Boot application, the spring-boot-starter-test starter already contains junit-jupiter, and the Spring Boot parent manages the JUnit version. So we add no JUnit dependency and no junit-bom of our own. For example, Spring Boot 3.5.16 brings JUnit Jupiter 5.12.2.
When we need a newer JUnit 5 release in Spring Boot 3.5, we override the junit-jupiter.version property instead of declaring the artifacts with versions. Spring Boot imports the junit-bom with this property, so the Platform artifacts also change to 1.14.4.
<properties>
<junit-jupiter.version>5.14.4</junit-jupiter.version>
</properties>
Spring Boot 4.x manages JUnit 6 (Spring Boot 4.1.1 brings JUnit 6.0.3), so a Spring Boot 4 project is a JUnit 6 project.
6. JUnit 5 or JUnit 6?
JUnit 6 is the successor of JUnit 5 and keeps the same Jupiter API, so @Test, Assertions and the extensions stay as they are. The changes that matter for the pom.xml are the Java baseline and the version numbers.
| JUnit 5 (5.14.4) | JUnit 6 (6.1.3) | |
|---|---|---|
| Minimum Java version | 8 | 17 |
| Version numbers | Jupiter 5.x, Platform 1.x | one version, 6.x, for all artifacts |
| JUnit Vintage engine | supported | deprecated |
| Spring Boot | 3.x | 4.x |
| Maven setup | junit-bom 5.14.4 + junit-jupiter | junit-bom 6.1.3 + junit-jupiter |
For a project on Java 8 to 16 or on Spring Boot 3, we stay on JUnit 5.14.4. For a project on Java 17 or later, the upgrade is a version change in the junit-bom, as the JUnit 6 Maven dependency setup shows. For Gradle builds, see JUnit 5 with Gradle.
7. JUnit 5 Maven FAQs
7.1. Do We Need Both junit-jupiter-api and junit-jupiter-engine?
No, the single junit-jupiter artifact contains both, and the parameterized tests as well. With Surefire 3.6.0, even a project with only junit-jupiter-api runs its tests, because Surefire adds the matching engine to the test run when it is missing. We still declare junit-jupiter, so the IDE and other tools find the engine too.
7.2. Why Does mvn test Say “Tests run: 0” With JUnit 5?
Either Surefire is older than 2.22.0, or the test classes don’t match the Surefire naming patterns. We declare Surefire 3.6.0 as in section 3.1, and we check that the class names end with Test or Tests. Also, the test classes must be in src/test/java, and the methods must use org.junit.jupiter.api.Test, not the JUnit 4 org.junit.Test.
7.3. Which Scope Do JUnit 5 Dependencies Need?
All JUnit dependencies use the test scope, so they are on the classpath when Maven compiles and runs the tests, but not in the packaged application.
8. Conclusion
A JUnit 5 Maven setup needs three things, namely the junit-bom in dependencyManagement, the junit-jupiter dependency in the test scope and a declared Surefire 3.x version. Failsafe uses the same JUnit dependencies for the integration test classes.
When something goes wrong, the cause is a Surefire version before 2.22.0, which runs zero tests without an error, or mixed JUnit versions, which fail with a NoClassDefFoundError. The BOM fixes the second problem. In Spring Boot projects, we let the Spring Boot parent manage the version and override junit-jupiter.version when needed.
9. References
- JUnit 5.14.4 User Guide: Build Support
- JUnit 5.14.4 User Guide: Overview
- JUnit 6.0.0 Release Notes
- Maven Surefire: Using JUnit 5 Platform
- Maven Failsafe Plugin
- JUnit BOM on Maven Central
Happy Learning !!
Hi
Thanks for the info
what if, my project has few old test cases developed using JUnit4
and new methods developed in JUnit5
does it solve my problem if we combine both plugins and dependencies?
Do we need to take any additional issues in this case ?
Add junit 5 dependencies including vintage. This setup will run both kind of tests.