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.
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.
<?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.
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.
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.
<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.
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.
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.
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.
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
| Parameter | Property | Description |
|---|---|---|
test | test | tests to run |
includes, excludes | patterns of test classes | |
groups, excludedGroups | groups, excludedGroups | JUnit tag expressions |
skipTests | skipTests | do not run tests |
testFailureIgnore | maven.test.failure.ignore | do not fail the build |
rerunFailingTestsCount | surefire.rerunFailingTestsCount | rerun failed tests |
forkCount | forkCount | number of test JVMs |
argLine | argLine | JVM options of the test JVM |
systemPropertyVariables | system 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
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
List all Java tutorials.