ZetCode

Maven Surefire Plugin

last modified November 13, 2026

In this article we run unit tests with the Maven Surefire Plugin. We run all tests and single tests, configure which classes are tests, filter tests by JUnit tags, handle failures and flaky tests, skip tests, and run tests in parallel. The examples use Maven 3.10, Surefire 3.6.1, JUnit 6, and Java 25.

Surefire is bound to the test phase of the default lifecycle. Its test goal runs the tests compiled from src/test/java in a separate JVM, prints a summary, writes reports, and fails the build when a test fails. Integration tests, which run after packaging, are the task of the Failsafe plugin.

The project

Our project has one class with simple text statistics.

stats/src/main/java/com/example/text/TextStats.java
package com.example.text;

import java.util.Arrays;
import java.util.Comparator;

public class TextStats {

    public static int wordCount(String text) {
        return text.isBlank() ? 0 : text.strip().split("\\s+").length;
    }

    public static String longestWord(String text) {
        return Arrays.stream(text.split("\\s+"))
                .max(Comparator.comparingInt(String::length))
                .orElse("");
    }

    public static long vowelCount(String text) {
        return text.toLowerCase().chars().filter(c -> "aeiou".indexOf(c) >= 0).count();
    }
}

The class counts words and vowels and finds the longest word of a text.

stats/pom.xml
<?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.example</groupId>
    <artifactId>stats</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>6.1.3</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-surefire-plugin</artifactId>
                <version>3.6.1</version>
            </plugin>
        </plugins>
    </build>
</project>

The POM adds JUnit Jupiter in the test scope and pins the version of the Surefire plugin. Maven 3.10 uses Surefire 3.6.0 by default; pinning the version keeps the build reproducible across Maven versions.

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

The junit-jupiter aggregator brings the API, the parameterized tests module, and the engine. Surefire finds the JUnit Platform engine on the test classpath and uses its JUnit Platform provider; no further configuration is needed.

stats/src/test/java/com/example/text/TextStatsTest.java
package com.example.text;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class TextStatsTest {

    @Test
    void countsWords() {
        assertEquals(3, TextStats.wordCount("an old  falcon"));
    }

    @Test
    void countsWordsInBlankText() {
        assertEquals(0, TextStats.wordCount("   "));
    }

    @Test
    void findsLongestWord() {
        assertEquals("falcon", TextStats.longestWord("an old falcon"));
    }

    @Test
    void countsVowels() {
        assertEquals(4, TextStats.vowelCount("An old falcon"));
    }

    @Test
    @Tag("slow")
    void countsWordsInLargeText() {
        String text = "word ".repeat(5_000_000);
        assertEquals(5_000_000, TextStats.wordCount(text));
    }
}

The test class has five tests. The last one creates a large string and is tagged as slow.

Running tests

The mvn test command runs all phases up to test: it compiles the main and test sources and runs the tests.

Surefire reports the provider it detected, the result for each test class, and the total. For each class, it writes a plain text report and an XML report into the target/surefire-reports directory. The XML reports are read by CI servers to display test results.

$ mvn test
--- surefire:3.6.1:test (default-test) @ stats ---
Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
 T E S T S
Running com.example.text.TextStatsTest
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.524 s -- in com.example.text.TextStatsTest
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
$ ls target/surefire-reports
com.example.text.TextStatsTest.txt
TEST-com.example.text.TextStatsTest.xml

Selecting tests

The test property selects the tests to run. It takes class names, optionally with a method after #, and supports the * wildcard. Multiple patterns are separated with commas.

The pattern TextStatsTest#countsWords* runs the three methods whose names start with countsWords. The second command runs the TextStatsCheck class, which is not run by default, as we will see in the next section; an explicit -Dtest overrides the default includes. When no test matches the pattern, the build fails. In a multi-module project, where a pattern matches tests in some modules only, we add -Dsurefire.failIfNoSpecifiedTests=false.

$ mvn test -Dtest='TextStatsTest#countsWords*'
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.445 s -- in com.example.text.TextStatsTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
$ mvn test -Dtest=TextStatsCheck
Running com.example.text.TextStatsCheck
BUILD SUCCESS
$ mvn test -Dtest=NoSuch
No tests matching pattern "NoSuch" were executed! (Set -Dsurefire.failIfNoSpecifiedTests=false to ignore this error.)

Includes and excludes

By default, Surefire runs the classes whose names match **/Test*.java, **/*Test.java, **/*Tests.java, or **/*TestCase.java. Our project has another test class that does not follow this convention.

stats/src/test/java/com/example/text/TextStatsCheck.java
package com.example.text;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class TextStatsCheck {

    @Test
    void longestWordOfEmptyText() {
        assertEquals("", TextStats.longestWord(""));
    }
}

The class is compiled, but Surefire never runs it. We can rename it, or change the patterns with the includes and excludes parameters.

pom.xml (Surefire plugin)
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Check.java</include>
        </includes>
        <excludedGroups>slow</excludedGroups>
    </configuration>
</plugin>

The includes element replaces the default patterns, so we list **/*Test.java again. The excludedGroups element excludes tests tagged as slow; we cover tags in the next section.

Both classes are run now, and the slow test is left out. An exclusion in the POM cannot be undone from the command line: requesting the slow group gives no tests, because the excluded groups still apply.

$ mvn test
Running com.example.text.TextStatsCheck
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.054 s -- in com.example.text.TextStatsCheck
Running com.example.text.TextStatsTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.015 s -- in com.example.text.TextStatsTest
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
$ mvn test -Dgroups=slow
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Tags

JUnit tags let us group tests. Surefire selects them with the groups and excludedGroups parameters, which are also available as the groups and excludedGroups properties. The parameters accept tag expressions such as fast | integration or !slow.

With the original POM, the first command runs the four fast tests, and the second one only the slow test. A common setup excludes slow tests by default in the POM through a property, for example <excludedGroups>${test.excluded}</excludedGroups>, so that CI can run them with a different value.

$ mvn test -DexcludedGroups=slow
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 s -- in com.example.text.TextStatsTest
BUILD SUCCESS
$ mvn test -Dgroups=slow
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.465 s -- in com.example.text.TextStatsTest
BUILD SUCCESS

Test failures

We add a test that finds a bug in the longestWord method.

src/test/java/com/example/text/TextStatsPunctuationTest.java
package com.example.text;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class TextStatsPunctuationTest {

    @Test
    void ignoresPunctuation() {
        assertEquals("falcon", TextStats.longestWord("a falcon, a hawk"));
    }
}

The method splits the text on whitespace, so the punctuation stays attached to the words.

Surefire prints the failed test with the assertion message and fails the build. The maven.test.failure.ignore property lets the build continue despite failures, which is useful in CI jobs that collect reports from all modules; the failures are still reported. The text report contains the stack trace.

$ mvn test
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.473 s -- in com.example.text.TextStatsTest
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.011 s <<< FAILURE! -- in com.example.text.TextStatsPunctuationTest
com.example.text.TextStatsPunctuationTest.ignoresPunctuation -- Time elapsed: 0.007 s <<< FAILURE!
  TextStatsPunctuationTest.ignoresPunctuation:11 expected: <falcon> but was: <falcon,>
Tests run: 6, Failures: 1, Errors: 0, Skipped: 0
BUILD FAILURE
$ mvn test -Dmaven.test.failure.ignore=true
Tests run: 6, Failures: 1, Errors: 0, Skipped: 0
BUILD SUCCESS
$ cat target/surefire-reports/com.example.text.TextStatsPunctuationTest.txt
-------------------------------------------------------------------------------
Test set: com.example.text.TextStatsPunctuationTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.008 s <<< FAILURE! -- in com.example.text.TextStatsPunctuationTest
com.example.text.TextStatsPunctuationTest.ignoresPunctuation -- Time elapsed: 0.005 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <falcon> but was: <falcon,>
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1199)
	at com.example.text.TextStatsPunctuationTest.ignoresPunctuation(TextStatsPunctuationTest.java:11)

Flaky tests

A flaky test sometimes passes and sometimes fails without a change in the code, typically because of timing or shared state. The following test simulates one: it fails on the first run and passes on the second.

src/test/java/com/example/text/FlakyTest.java
package com.example.text;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

class FlakyTest {

    private static int runs = 0;

    @Test
    void failsOnFirstRun() {
        runs++;
        assertTrue(runs > 1, "run " + runs);
    }
}

The static counter survives between runs because all reruns happen in the same JVM.

The rerunFailingTestsCount parameter reruns a failed test up to the given number of times. A test that passes on a rerun is reported as a flake, and the build succeeds. Reruns should be a temporary measure; the flakes in the summary show which tests need to be fixed.

$ mvn test -Dsurefire.rerunFailingTestsCount=2
Running com.example.text.FlakyTest
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.012 s <<< FAILURE! -- in com.example.text.FlakyTest
com.example.text.FlakyTest.failsOnFirstRun -- Time elapsed: 0.008 s <<< FAILURE!
	at com.example.text.FlakyTest.failsOnFirstRun(FlakyTest.java:14)
Running com.example.text.FlakyTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.example.text.FlakyTest
Flakes: 
com.example.text.FlakyTest.failsOnFirstRun
  Run 1: FlakyTest.failsOnFirstRun:14 run 1 ==> expected: <true> but was: <false>
  Run 2: PASS
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Flakes: 1
BUILD SUCCESS

Skipping tests

There are two properties to skip tests. The skipTests property skips running the tests, but compiles them. The maven.test.skip property skips the compilation of the test sources as well; the Compiler, Resources, and Surefire plugins all honor it.

Both commands package the application. With -DskipTests, a broken test that does not compile still fails the build, which is usually what we want.

$ mvn clean package -DskipTests
--- compiler:3.16.0:testCompile (default-testCompile) @ stats ---
--- surefire:3.6.1:test (default-test) @ stats ---
Tests are skipped.
BUILD SUCCESS
$ mvn clean package -Dmaven.test.skip=true
--- compiler:3.16.0:testCompile (default-testCompile) @ stats ---
Not compiling test sources
--- surefire:3.6.1:test (default-test) @ stats ---
Tests are skipped.
BUILD SUCCESS

Parallel tests

JUnit Jupiter can run tests in parallel within one JVM. The feature is configured with JUnit configuration parameters, which we put into the junit-platform.properties file.

parallel/src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4

The file enables parallel execution, makes it the default for all test methods, and uses four threads. Without the second line, only classes and methods annotated with @Execution(CONCURRENT) would run in parallel.

parallel/src/test/java/com/example/ParallelTest.java
package com.example;

import org.junit.jupiter.api.Test;

class ParallelTest {

    private void work(String name) throws InterruptedException {
        Thread.sleep(1000);
        System.out.printf("%s on %s%n", name, Thread.currentThread().getName());
    }

    @Test
    void first() throws InterruptedException {
        work("first");
    }

    @Test
    void second() throws InterruptedException {
        work("second");
    }

    @Test
    void third() throws InterruptedException {
        work("third");
    }

    @Test
    void fourth() throws InterruptedException {
        work("fourth");
    }
}

Each test sleeps for one second and prints the name of its thread.

The four tests run in different threads and finish in about one second. We can switch parallel execution off with a system property, because Surefire passes Maven user properties to the test JVM and JUnit reads configuration parameters from system properties; the tests then run one after another in the main thread and take four seconds. Surefire itself can also run tests in several JVMs with the forkCount parameter, which isolates tests that share static state.

$ mvn test
first on ForkJoinPool-1-worker-4
second on ForkJoinPool-1-worker-3
fourth on ForkJoinPool-1-worker-2
third on ForkJoinPool-1-worker-1
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.080 s -- in com.example.ParallelTest
$ mvn test -Djunit.jupiter.execution.parallel.enabled=false
fourth on main
second on main
first on main
third on main
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.106 s -- in com.example.ParallelTest

Useful parameters

ParameterPropertyDescription
testtesttests to run
includes, excludespatterns of test classes
groups, excludedGroupsgroups, excludedGroupsJUnit tag expressions
skipTestsskipTestsdo not run tests
testFailureIgnoremaven.test.failure.ignoredo not fail the build
rerunFailingTestsCountsurefire.rerunFailingTestsCountrerun failed tests
forkCountforkCountnumber of test JVMs
argLineargLineJVM options of the test JVM
systemPropertyVariablessystem properties for the tests

The argLine parameter is also used by tools that attach a Java agent to the test JVM, such as JaCoCo.

Source

Maven Surefire Plugin

In this article we have run JUnit tests with the Maven Surefire Plugin, selected and filtered tests, handled failures and flaky tests, and run tests in parallel.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.