Maven Surefire Plugin Configuration

8 min read

You have been running mvn test since Chapter 1, but with minimal Surefire configuration. In production projects, Surefire is the bridge between what JUnit discovers and what the build system executes: it filters by tag, injects system properties, enables parallel execution, and separates unit tests from integration tests. This lesson covers the configuration options you will actually use day-to-day, the difference between Surefire and Failsafe, and how to pass browser and environment properties cleanly without changing test code.

Baseline configuration

The minimum Surefire configuration for JUnit 5 is just the version declaration:

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

From here you add configuration progressively. Everything below goes inside <configuration>.

Tag filtering

Run only tests tagged smoke and exclude tests tagged slow:

<configuration>
    <groups>smoke</groups>
    <excludedGroups>slow</excludedGroups>
</configuration>

Override at the command line without changing pom.xml:

# Run smoke tests
mvn test -Dgroups=smoke
 
# Run regression, excluding slow tests
mvn test -Dgroups=regression -DexcludedGroups=slow
 
# Boolean expression: smoke AND api
mvn test "-Dgroups=smoke & api"

The <groups> value is passed directly to the JUnit Platform tag expression engine, so all the Boolean syntax (&, |, !) works both in pom.xml and on the command line.

Passing system properties to tests

Tests should read environment-specific values (browser choice, base URL, credentials) from system properties — not hardcode them. Surefire injects system properties into the test JVM:

<configuration>
    <systemPropertyVariables>
        <browser>${browser}</browser>
        <baseUrl>${baseUrl}</baseUrl>
        <headless>${headless}</headless>
    </systemPropertyVariables>
</configuration>

Default values in pom.xml using Maven properties:

<properties>
    <browser>chrome</browser>
    <baseUrl>http://localhost:8080</baseUrl>
    <headless>false</headless>
</properties>

Override at runtime:

mvn test -Dbrowser=firefox -DbaseUrl=https://staging.myapp.com -Dheadless=true

Inside a test or extension, read with System.getProperty("browser"). This keeps the test code environment-agnostic — the same class runs locally against localhost and in CI against the staging URL without any code changes.

Maven profiles for different environments

For repeatable multi-environment configuration, define Maven profiles:

<profiles>
    <profile>
        <id>staging</id>
        <properties>
            <baseUrl>https://staging.myapp.com</baseUrl>
            <headless>true</headless>
        </properties>
    </profile>
    <profile>
        <id>production-smoke</id>
        <properties>
            <baseUrl>https://myapp.com</baseUrl>
            <groups>smoke</groups>
            <headless>true</headless>
        </properties>
    </profile>
</profiles>

Activate: mvn test -Pstaging. The CI pipeline uses -Pstaging; the local developer runs mvn test and gets the default localhost configuration.

Parallel execution via Surefire

Parallel settings can live in junit-platform.properties (as shown in Chapter 4) or directly in the Surefire <configuration>:

<configuration>
    <configurationParameters>
        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
    </configurationParameters>
</configuration>

The <configurationParameters> block is passed through to the JUnit Platform launcher. This is useful when you want the parallel settings to be part of the Maven build definition rather than a separate properties file.

Surefire vs Failsafe — unit vs integration tests

Surefire and Failsafe are two plugins with a deliberate design split:

Surefire runs during the test phase. It discovers classes named *Test.java, Test*.java, or *Tests.java. If any test fails, it marks the Maven build as failed immediately and stops processing further phases. This is the right behaviour for unit tests — fast feedback, fail fast.

Failsafe runs during the integration-test phase (after the application is packaged and started). It discovers classes named *IT.java or *ITCase.java. Crucially, Failsafe does not fail the build when tests fail — it records the failure and lets the verify phase run, which then fails the build. This allows post-test teardown (stopping the test server, generating reports) to happen even when tests fail.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.2.5</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Naming convention enforces the split without any configuration:

src/test/java/
  com/mycompany/tests/
    CalculatorTest.java       ← Surefire picks this up (unit)
    LoginIT.java              ← Failsafe picks this up (integration/Selenium)

Run everything: mvn verify. Run only unit tests: mvn test. Run only integration tests: mvn failsafe:integration-test failsafe:verify.

Test class discovery patterns

The default Surefire discovery pattern covers most naming conventions. Override it when you need something different:

<configuration>
    <includes>
        <include>**/*Test.java</include>
        <include>**/*Tests.java</include>
        <include>**/*Spec.java</include>
    </includes>
    <excludes>
        <exclude>**/Abstract*.java</exclude>
        <exclude>**/Base*.java</exclude>
    </excludes>
</configuration>

The excludes pattern is important for base classes like BaseTest.java — if they contain @Test methods, Surefire will try to run them directly, which usually fails because the base class assumes fields set by subclass constructors.

Maven test lifecycle

⚠️ Common mistakes

  • Using Surefire for Selenium tests. Because Surefire fails fast, a Selenium test failure stops the build before driver.quit() in @AfterEach can clean up — or before Allure can generate a report. For end-to-end tests that need post-failure cleanup and reporting, use Failsafe and name them *IT.java.
  • Hardcoding base URL in test code. driver.get("https://staging.myapp.com/login") in the test class means you need to edit source code to run against a different environment. Read from System.getProperty("baseUrl", "http://localhost:8080") — the default keeps local runs working without any properties, and CI overrides with -DbaseUrl.
  • Forgetting <goal>verify</goal> in Failsafe. Without the verify goal, Failsafe records integration test failures but never acts on them — the build shows SUCCESS even when all your integration tests failed. You need both integration-test and verify in the Failsafe execution goals.

🎯 Practice task

Wire up a complete Surefire + Failsafe configuration. 25–35 minutes.

  1. Take your existing test project. Split the tests into LoginTest.java (unit-style, named *Test) and LoginIT.java (integration/Selenium, named *IT). Add the Failsafe plugin.
  2. Run mvn test — confirm only LoginTest runs. Run mvn verify — confirm both run.
  3. Add tag filtering. Tag two tests @Tag("smoke") and one @Tag("slow"). Add <groups>smoke</groups> to the Surefire config. Run mvn test — only the two tagged tests should run.
  4. System property injection. Add a baseUrl system property variable in Surefire. In one test, print System.getProperty("baseUrl"). Run mvn test -DbaseUrl=https://staging.myapp.com — confirm the URL appears in the output.
  5. Maven profile. Create a staging profile with <baseUrl>https://staging.myapp.com</baseUrl> and <groups>smoke</groups>. Activate it with mvn test -Pstaging and confirm only smoke tests run against the staging URL.
  6. Stretch — parallel. Add <configurationParameters> to Surefire with junit.jupiter.execution.parallel.enabled = true and parallelism = 2. Add Thread.sleep(2000) to four tests. Run without parallel (record time), then enable parallel (record time again). Confirm roughly 2× speedup.

Next lesson: generating HTML and Allure reports from JUnit 5 test results.

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