Setting Up JUnit 5 with Maven

8 min read

The previous lesson established that JUnit 5 is three separate modules. That separation shows up immediately in the Maven setup: you need the right artifact, the right Surefire version, and — unlike TestNG — no XML suite file to get started. This lesson builds a project you can use for every exercise in this course, and explains why each configuration choice exists rather than just telling you what to copy.

The Maven dependency

JUnit 5 publishes a convenience artifact called junit-jupiter that bundles everything you need for writing and running Jupiter tests:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
</dependency>

This single dependency pulls in three things transitively: junit-jupiter-api (the annotations and assertion API you write against), junit-jupiter-engine (the TestEngine that executes Jupiter tests), and junit-jupiter-params (the parameterised test support). You can declare them separately for fine-grained control, but junit-jupiter is simpler and correct for most projects.

Two things worth noting: <scope>test</scope> keeps JUnit off your production classpath — same principle as TestNG. And the version 5.10.2 is the current stable release as of this writing; check central.sonatype.com for the latest.

The Surefire plugin — the critical piece

This is where most first-time JUnit 5 setups break. Maven ships with a bundled Surefire version that predates JUnit 5. That old version does not understand the JUnit Platform and will run zero tests — you'll see Tests run: 0, Failures: 0 with no error, which is the most confusing outcome possible. Fix it by declaring Surefire explicitly:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

Surefire 2.22.0 was the first version with JUnit Platform support. Version 3.2.5 is the current stable — use it. No extra configuration is needed; Surefire auto-detects the Jupiter engine on the classpath.

Full pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany.tests</groupId>
    <artifactId>junit5-suite</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

Java 17 is the minimum recommended version for JUnit 5.10. It is the current LTS and what most teams use. If your environment requires Java 11, change the compiler properties to 11 — everything in this course compiles on 11.

Your first test class

package com.mycompany.tests;
 
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
 
class CalculatorTest {
 
    @Test
    void addition() {
        assertEquals(4, 2 + 2);
    }
 
    @Test
    void division() {
        assertEquals(2.5, 10.0 / 4.0, 0.001);
    }
}

Three things to notice that are different from TestNG and JUnit 4:

No public modifier. Jupiter does not require test classes or methods to be public. Package-private (no modifier) is fine and is the recommended style. This is a deliberate JUnit 5 design choice — access modifiers on test classes have no semantic meaning since tests are always called reflectively by the framework.

Static imports for assertions. Assertions.assertEquals(...) works, but import static org.junit.jupiter.api.Assertions.* is the convention. Every IDE will offer this as a quick-fix when you type assertEquals without an import.

No test runner annotation. In JUnit 4 you had @RunWith(...). In TestNG you had the testng.xml runner. In Jupiter there is nothing — the engine discovers any class containing @Test methods automatically.

Running tests

# Run everything
mvn clean test
 
# Run a single class
mvn test -Dtest=CalculatorTest
 
# Run a single method
mvn test -Dtest=CalculatorTest#addition
 
# Run all tests matching a pattern
mvn test -Dtest="Calculator*"

In IntelliJ: right-click a method → Run. Right-click the class → Run. Right-click the src/test/java directory → Run All Tests. The JUnit 5 runner is built in — no plugin to install.

After a successful run you'll see:

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Surefire writes XML results to target/surefire-reports/. These are what Jenkins and GitHub Actions parse for test trend graphs.

The full setup flow

Step 1 of 6

Create Maven project

IntelliJ → File → New Project → Maven Archetype → maven-archetype-quickstart. GroupId: com.mycompany.tests, ArtifactId: junit5-suite. Or run: mvn archetype:generate -DgroupId=com.mycompany.tests -DartifactId=junit5-suite -DarchetypeArtifactId=maven-archetype-quickstart.

Project layout

junit5-suite/
├── pom.xml
└── src/
    └── test/
        ├── java/
        │   └── com/mycompany/tests/
        │       ├── CalculatorTest.java
        │       └── ...
        └── resources/
            └── testdata/          ← CSV files for parameterised tests

Unlike TestNG, there is no testng.xml equivalent. JUnit 5 discovers tests by scanning for @Test methods. You can add a junit-platform.properties file to src/test/resources/ for configuration options like parallel execution (covered in Chapter 4), but it is not required to get started.

⚠️ Common mistakes

  • Tests run: 0 after adding JUnit 5. This almost always means Surefire is the bundled old version that doesn't know the JUnit Platform. Add <version>3.2.5</version> to the Surefire plugin declaration and re-run. Second possibility: your test class or method name doesn't end in Test or start with Test — JUnit 5 discovers by annotation, not name, but Surefire's class-scanner still applies its default pattern. Move the class to a properly named file or add **/*Test.java to Surefire's includes.
  • public on every method out of JUnit 4 habit. It compiles fine, but Jupiter neither requires nor benefits from it. Dropping public is a visible signal that this is JUnit 5 code, not migrated JUnit 4.
  • Missing import static org.junit.jupiter.api.Assertions.*. If assertEquals doesn't resolve and IntelliJ offers org.junit.Assert, you have a JUnit 4 jar on the classpath (perhaps from an archetype). Add the static import from Jupiter and remove the JUnit 4 jar.

🎯 Practice task

Build the skeleton you'll use for the whole course. 20–25 minutes.

  1. Create junit5-suite with the pom.xml shown. Run mvn dependency:tree — confirm you see junit-jupiter-5.10.2, junit-jupiter-api, junit-jupiter-engine, and junit-jupiter-params all resolved.
  2. Write CalculatorTest.java with four test methods: addition, subtraction, multiplication, division. Run mvn clean test. Confirm four tests pass.
  3. Force a failure on one test. Read the console output and find the exact line that says which method failed and what the mismatch was. Revert.
  4. Try the name-pattern filter. Run mvn test -Dtest=CalculatorTest#addition. Confirm only one test runs.
  5. Open target/surefire-reports/. Find the XML file for CalculatorTest. Open it and read the structure — this is what CI tools parse for test reporting.
  6. Stretch — add a second test class. Create StringUtilsTest.java with two tests. Run mvn test and confirm both classes execute without any configuration change.

Next lesson: writing meaningful tests with @Test and the full assertion toolkit.

// tip to track lessons you complete and pick up where you left off across devices.