JUnit 5 Maven Dependency: junit-bom, Surefire and pom.xml

Add JUnit 5 to a Maven project with junit-bom and the junit-jupiter dependency, then run the tests with Surefire and Failsafe 3.6.0. Covers every JUnit 5 artifact, JUnit 4 tests with Vintage, Spring Boot versions and the zero-tests problem.

Junit 5 jupiter engine dependency tree

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.
Diagram of the JUnit 5 Maven artifacts. Maven Surefire calls the JUnit Platform launcher, which runs two test engines: junit-jupiter-engine for JUnit 5 tests and junit-vintage-engine for JUnit 4 tests. The aggregate artifact junit-jupiter contains junit-jupiter-api, junit-jupiter-params and junit-jupiter-engine. The junit-bom box sets one version for all of them.
Surefire talks to the JUnit Platform, the Platform runs the engines, and the single junit-jupiter dependency brings the API, the parameterized tests and the Jupiter engine.

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 artifactWhat it containsWhen we add it
org.junit.jupiter:junit-jupiteraggregate of junit-jupiter-api, junit-jupiter-params and junit-jupiter-enginealways (recommended)
org.junit.jupiter:junit-jupiter-api@Test, lifecycle annotations, Assertions, Assumptionsonly when we list the modules one by one
org.junit.jupiter:junit-jupiter-params@ParameterizedTest and the argument sourcesincluded in junit-jupiter
org.junit.jupiter:junit-jupiter-enginethe TestEngine that runs Jupiter testsincluded in junit-jupiter, needed at test runtime only
org.junit.platform:junit-platform-suite@Suite, @SelectPackages and the suite enginefor test suites
org.junit.platform:junit-platform-launcherthe API that IDEs and build tools use to start testsSurefire 3.x adds it itself; Gradle needs it as testRuntimeOnly
org.junit.vintage:junit-vintage-enginethe engine for JUnit 3 and JUnit 4 testswhile old JUnit 4 tests still exist
org.junit:junit-bomno code, only version numbersalways, 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 version817
Version numbersJupiter 5.x, Platform 1.xone version, 6.x, for all artifacts
JUnit Vintage enginesupporteddeprecated
Spring Boot3.x4.x
Maven setupjunit-bom 5.14.4 + junit-jupiterjunit-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

Happy Learning !!

Source Code on Github

Leave a Comment

  1. 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 ?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.