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=trueInside 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@AfterEachcan 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 fromSystem.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 theverifygoal, Failsafe records integration test failures but never acts on them — the build shows SUCCESS even when all your integration tests failed. You need bothintegration-testandverifyin the Failsafe execution goals.
🎯 Practice task
Wire up a complete Surefire + Failsafe configuration. 25–35 minutes.
- Take your existing test project. Split the tests into
LoginTest.java(unit-style, named*Test) andLoginIT.java(integration/Selenium, named*IT). Add the Failsafe plugin. - Run
mvn test— confirm onlyLoginTestruns. Runmvn verify— confirm both run. - Add tag filtering. Tag two tests
@Tag("smoke")and one@Tag("slow"). Add<groups>smoke</groups>to the Surefire config. Runmvn test— only the two tagged tests should run. - System property injection. Add a
baseUrlsystem property variable in Surefire. In one test, printSystem.getProperty("baseUrl"). Runmvn test -DbaseUrl=https://staging.myapp.com— confirm the URL appears in the output. - Maven profile. Create a
stagingprofile with<baseUrl>https://staging.myapp.com</baseUrl>and<groups>smoke</groups>. Activate it withmvn test -Pstagingand confirm only smoke tests run against the staging URL. - Stretch — parallel. Add
<configurationParameters>to Surefire withjunit.jupiter.execution.parallel.enabled = trueandparallelism = 2. AddThread.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.