Apache Maven Surefire — Architecture Overview
What is Surefire?
Apache Maven Surefire is the test execution framework for Maven. It ships three plugins:
| Plugin | Purpose |
|---|---|
| maven-surefire-plugin | Runs unit tests during the test phase |
| maven-failsafe-plugin | Runs integration tests during integration-test / verify phases |
| maven-surefire-report-plugin | Generates HTML test reports from XML results |
Surefire supports JUnit 4 (4.12+), JUnit 5 (Jupiter), and TestNG (6.14.3+).
Until 3.5.x, each type was executed via a dedicated provider module. From 3.6.0 on, there is only one unified provider.
Tests execute in a forked JVM that communicates results back to Maven through a binary event stream protocol.
Module Map
The project is a multi-module Maven reactor with 14+ modules organized in layers.
graph TD
subgraph "Layer 5 — Maven Plugins"
MSP["maven-surefire-plugin"]
MFP["maven-failsafe-plugin"]
MRP["maven-surefire-report-plugin"]
end
subgraph "Layer 4 — Shared Plugin Logic"
MSC["maven-surefire-common<br/><i>AbstractSurefireMojo, ForkStarter, BooterSerializer</i>"]
end
subgraph "Layer 3 — Forked JVM Bootstrap"
SB["surefire-booter<br/><i>ForkedBooter, PpidChecker, IsolatedClassLoader</i>"]
end
subgraph "Layer 2 — Extension Points"
SEA["surefire-extensions-api<br/><i>ForkNodeFactory, reporters — plugin side</i>"]
SES["surefire-extensions-spi<br/><i>MasterProcessChannelProcessorFactory — fork side</i>"]
end
subgraph "Layer 1 — Core API"
SA["surefire-api<br/><i>SurefireProvider SPI, events, stream codec</i>"]
end
subgraph "Layer 0 — Foundations"
SLA["surefire-logger-api"]
SSU["surefire-shared-utils<br/><i>Shaded: commons-lang3, commons-io, etc.</i>"]
end
subgraph "Providers — Loaded in Forked JVM"
JP["surefire-junit-platform<br/><i>unified provider for all JUnit 3+ versions</i>"]
end
subgraph "Shading"
SF["surefire-shadefire<br/><i>Self-test: shades provider for Surefire's own build</i>"]
end
subgraph "Reporting"
SRP["surefire-report-parser"]
end
MSP --> MSC
MFP --> MSC
MSC --> SA
MSC --> SB
MSC --> SEA
SB --> SA
SB --> SES
SEA --> SA
SES --> SA
SA --> SLA
SA --> SSU
SB --> SSU
MRP --> SRP
SRP --> SLA
Key relationships
- Both plugins (
surefire-plugin,failsafe-plugin) share all logic viamaven-surefire-common. The failsafe plugin adds pre/post-integration-test lifecycle binding and a different default report directory. - Providers are not compile dependencies of the plugins. They are resolved at runtime by
SurefireDependencyResolverand loaded in the forked JVM's classpath. surefire-shared-utilsshades commons-lang3, commons-io, commons-compress, and maven-shared-utils intoorg.apache.maven.surefire.shared.*to prevent version conflicts with user projects.surefire-shadefireshades the entiresurefire-junit-platformprovider intoorg.apache.maven.shadefire.*so Surefire can use a different version of itself to run its own unit tests during its build.
Forked JVM Architecture
The most important architectural concept: tests never run in the Maven JVM. They execute in a separate forked process.
sequenceDiagram
participant User as mvn test
participant Mojo as AbstractSurefireMojo<br/>(Maven JVM)
participant FS as ForkStarter
participant BS as BooterSerializer
participant Fork as ForkedBooter.main()<br/>(Forked JVM)
participant Prov as SurefireProvider
participant FC as ForkClient
User->>Mojo: execute()
Mojo->>Mojo: Auto-detect provider<br/>(ProviderDetector)
Mojo->>Mojo: Resolve provider classpath<br/>(SurefireDependencyResolver)
Mojo->>FS: fork()
FS->>BS: Serialize config to .properties file
FS->>FS: Build JVM command line<br/>(ForkConfiguration)
FS->>Fork: Launch process<br/>(java -cp ... ForkedBooter)
Fork->>Fork: setupBooter()<br/>- Read .properties file<br/>- Set system properties<br/>- Connect communication channel<br/>- Start PpidChecker (orphan detection)
Fork->>Prov: Load provider via IsolatedClassLoader
Fork->>Prov: invoke(testSet)
Prov->>Prov: Run tests (JUnit/TestNG/etc.)
loop For each test event
Prov->>Fork: Report event via RunListener
Fork-->>FC: Binary event stream<br/>(EventChannelEncoder → pipe/TCP)
FC-->>Mojo: Decoded Event objects<br/>(EventDecoder → ForkedProcessEventNotifier)
Mojo->>Mojo: Update console + write XML reports
end
Fork-->>FC: ControlByeEvent
FC-->>FS: Run complete
FS-->>Mojo: RunResult (pass/fail/skip counts)
Fork modes
| Mode | Behavior | Flag |
|---|---|---|
| forkCount=1 (default) | Single forked JVM, all test classes run sequentially | <forkCount>1</forkCount> |
| forkCount=N | N parallel forked JVMs | <forkCount>4</forkCount> |
| forkCount=NC | N × CPU cores parallel forks | <forkCount>1C</forkCount> |
| reuseForks=true (default) | Same JVM reused across test classes | <reuseForks>true</reuseForks> |
| reuseForks=false | New JVM per test class | <reuseForks>false</reuseForks> |
Fork configuration variants
The command line for the forked JVM is built by one of three ForkConfiguration implementations:
| Class | When used | Classpath strategy |
|---|---|---|
ClasspathForkConfiguration |
Default for classpath mode | -cp <full classpath> |
JarManifestForkConfiguration |
When classpath exceeds OS limits | Creates a temp JAR with Class-Path manifest entry |
ModularClasspathForkConfiguration |
Java 9+ module path | --module-path + --add-modules |
Provider Model
old Provider Model 3.5.x
graph LR
subgraph "Before — 3.5.x (5 providers)"
M1["AbstractSurefireMojo"] --> PD1["ProviderDetector<br/>(priority-based)"]
PD1 --> JP1["surefire-junit-platform"]
PD1 --> TNG1["surefire-testng"]
PD1 --> J471["surefire-junit47"]
PD1 --> J41["surefire-junit4"]
PD1 --> J31["surefire-junit3"]
end
new Provider Model since 3.6.0
graph LR
subgraph "After — 3.6.0 (1 provider)"
M2["AbstractSurefireMojo"] --> PD2["Simplified detection"]
PD2 --> JP2["surefire-junit-platform"]
JP2 --> VE["Vintage Engine<br/>(JUnit 3/4)"]
JP2 --> JE["Jupiter Engine<br/>(JUnit 5)"]
JP2 --> TE["TestNG Engine<br/>(TestNG)"]
end
The five ProviderInfo implementations (JUnit3ProviderInfo, JUnit4ProviderInfo, JUnitCoreProviderInfo, TestNgProviderInfo, JUnitPlatformProviderInfo) are collapsed into a unified detection path that always selects surefire-junit-platform. Framework-specific configuration (TestNG groups, JUnit 4 categories, parallel execution) is now mapped to JUnit Platform launcher configuration rather than being handled by framework-specific providers.
SurefireProvider SPI
Every test framework adapter implements SurefireProvider (in surefire-api):
public interface SurefireProvider {
// Returns test classes/suites — determines fork granularity
Iterable<Class<?>> getSuites();
// Executes tests; forkTestSet is null for "run all" or a Class for per-test forking
RunResult invoke(Object forkTestSet) throws TestSetFailedException, ReporterException;
// Graceful cancellation on timeout
void cancel();
}
Provider implementations
3.5.x: old implementation
| Provider | Module | Test framework | Key classes |
|---|---|---|---|
| JUnit 3 + POJO | surefire-junit3 |
JUnit 3.x, plain POJOs | JUnit3Provider, PojoTestSetExecutor |
| JUnit 4 | surefire-junit4 |
JUnit 4.0–4.6 | JUnit4Provider |
| JUnit 4.7+ | surefire-junit47 |
JUnit 4.7+ with parallel/categories | JUnitCoreProvider, ParallelComputerBuilder |
| TestNG | surefire-testng |
TestNG 4.7+ | TestNGProvider, TestNGExecutor |
| JUnit Platform | surefire-junit-platform |
JUnit 5, any JUnit Platform engine | JUnitPlatformProvider, LauncherAdapter |
| Framework | Before (3.5.x) | After (3.6.0) |
|---|---|---|
| JUnit 3 | Supported natively | Requires JUnit 4.12+ dependency (runs via Vintage Engine) |
| JUnit 4 | 4.0+ | 4.12+ (runs via Vintage Engine) |
| JUnit 5 | Any | Any (unchanged) |
| TestNG | 4.7+ | 6.14.3+ (runs via TestNG JUnit Platform Engine) |
| POJO tests | Supported | Removed |
Breaking Changes since 3.6.0
| Change | Impact | Mitigation |
|---|---|---|
| JUnit 3 standalone no longer supported | Projects using only JUnit 3 must add JUnit 4.12+ dependency | Add junit:junit:4.12 — test code unchanged |
| JUnit 4 < 4.12 no longer supported | Upgrade to JUnit 4.12+ | Mechanical version bump |
| TestNG < 6.14.3 no longer supported | Upgrade to TestNG 6.14.3+ | Mechanical version bump |
| POJO tests removed | Tests without framework annotations won't be found | Add @Test annotations |
| Category expression syntax changed | Complex boolean group expressions may behave differently under JUnit Platform tag expressions | Review and test group filter configurations |
| Provider selection changed | Manually configured legacy providers still work (via SPI) but auto-detection always chooses JUnit Platform | Pin surefire 3.5.x or add legacy provider as dependency |
JUnit 3 tests still work
JUnit 3 test code does not need to change. You only need to ensure your project depends on JUnit 4.12+ (which includes JUnit 3 API compatibility). The Vintage Engine executes JUnit 3 and JUnit 4 tests transparently.
POJO tests removed
The LegacyPojoStackTraceWriter and POJO test detection (PojoTestSetExecutor) are removed. Tests must use a recognized framework annotation (@Test from JUnit or TestNG).
Group / category filtering
The custom JavaCC-based category expression parser (surefire-grouper) is replaced by JUnit Platform's native tag expression syntax. For most users, <groups> and <excludedGroups> configuration works unchanged, but the underlying evaluation engine is different. Complex boolean expressions may need review.
Backward compatibility options
If upgrading causes issues, users have two fallback paths:
-
Pin Surefire 3.5.x — stay on the previous version:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.5.4</version> </plugin> -
Use a legacy provider as a plugin dependency (transitional):
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.6.0</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit3</artifactId> <version>3.5.4</version> </dependency> </dependencies> </plugin>
Communication Protocol
The forked JVM communicates with Maven through a binary event stream. Events flow one-way: fork → Maven. Commands flow the other way: Maven → fork.
Event types (fork → Maven)
The ForkedProcessEventType enum defines 21 opcodes:
| Category | Events |
|---|---|
| Test lifecycle | testset-starting, testset-completed, test-starting, test-succeeded, test-failed, test-skipped, test-error, test-assumption-failure |
| Console output | std-out-stream, std-out-stream-new-line, std-err-stream, std-err-stream-new-line |
| Logging | console-info-log, console-debug-log, console-warning-log, console-error-log |
| Control | bye, stop-on-next-test, next-test |
| System | sys-prop, jvm-exit-error |
Wire format
Each event is encoded as: :<magic>:<opcode>:<data>: using EventChannelEncoder (fork side) and decoded by EventDecoder (Maven side). The encoding uses ByteBuffer-backed channels for efficiency.
Transport channels
| Channel | Configuration | How it works |
|---|---|---|
| Pipe (default) | pipe:// |
stdout/stderr of forked process |
| TCP | tcp://host:port |
Socket connection, configured via Extensions SPI |
ClassLoader Isolation
The forked JVM uses a layered classloader strategy to prevent classpath conflicts:
graph TD
Boot["Bootstrap ClassLoader<br/>(JDK classes)"]
System["System ClassLoader<br/>(ForkedBooter + surefire-booter + surefire-api)"]
Isolated["IsolatedClassLoader<br/>(Provider + test classes + test dependencies)<br/><b>Child-first delegation</b>"]
Boot --> System
System --> Isolated
IsolatedClassLoader extends URLClassLoader with child-first class loading: it calls findClass() before delegating to the parent, ensuring test classes and provider classes take precedence over surefire internals.
Three classpaths are maintained:
| Classpath | Contents | Purpose |
|---|---|---|
| surefireClasspath | surefire-booter, surefire-api, extensions | System classloader — bootstrap |
| testClasspath | Test classes, test dependencies, provider | IsolatedClassLoader — test execution |
| inprocClasspath | Classes needed in both loaders | Bridging (e.g., SurefireProvider interface) |
For Java 9+, ModularClasspathConfiguration adds --module-path and --add-reads / --add-opens directives.
Shading Strategy
Two modules handle classpath isolation at build time:
surefire-shared-utils
Shades third-party libraries into the org.apache.maven.surefire.shared package:
| Original | Shaded to |
|---|---|
org.apache.commons.lang3 |
org.apache.maven.surefire.shared.lang3 |
org.apache.commons.io |
org.apache.maven.surefire.shared.io |
org.apache.commons.compress |
org.apache.maven.surefire.shared.compress |
org.apache.maven.shared.utils |
org.apache.maven.surefire.shared.utils |
This prevents version conflicts when user projects depend on different versions of these libraries.
surefire-shadefire
Shades the entire surefire-junit-platform provider (plus surefire-api, surefire-booter) into org.apache.maven.shadefire.*. This exists for one reason: Surefire must test itself. During the Surefire build, a different (shaded) version of the provider runs Surefire's own unit tests, avoiding conflicts with the version being compiled.
Process Monitoring
The forked JVM must detect when the parent Maven process dies to avoid becoming an orphan. Two complementary mechanisms exist, configured via ProcessCheckerType:
NATIVE mode (PpidChecker)
Periodically checks if the parent PID is still alive by running platform-specific commands:
| Platform | Command | Detection method |
|---|---|---|
| Linux/macOS | ps -o etime,pid <ppid> |
If elapsed time decreases between checks → PID was reused → parent is dead |
| Windows | PowerShell Win32_Process query |
If creation date changes → PID was reused |
Runs on a 1-second schedule. If the parent is detected as dead → Runtime.halt(1).
PING mode
The Maven process sends periodic NOOP commands through the communication channel. If no command is received within 30 seconds, the forked JVM considers the parent dead and halts.
Combined mode (ALL)
Both NATIVE and PING run simultaneously. Either one can trigger a halt.
Extensions SPI
Surefire provides two extension points that allow customizing fork communication and reporting:
Plugin-side extensions (surefire-extensions-api)
| Interface | Purpose |
|---|---|
ForkNodeFactory |
Creates ForkChannel instances for fork communication |
ForkChannel |
Bidirectional communication channel to a forked JVM |
EventHandler<E> |
Processes events received from forks |
StatelessReporter |
Generates XML test reports |
ConsoleOutputReporter |
Captures console output to files |
StatelessTestsetInfoReporter |
Writes testset summary to console/files |
Fork-side extensions (surefire-extensions-spi)
A single interface: MasterProcessChannelProcessorFactory — loaded via ServiceLoader in the forked JVM. It creates the encoder/decoder pair for communicating back to Maven. Supports pipe:// (default) and tcp:// transports.
Reporting
Test results flow through a layered reporting pipeline:
- Provider generates
ReportEntryevents (test started, succeeded, failed, etc.) TestSetRunListenerreceives events and delegates to registered reportersStatelessXmlReporterwritesTEST-*.xmlfiles (JUnit/Ant-compatible format)ConsoleOutputFileReporterwrites*-output.txtfiles with captured stdout/stderrDefaultReporterFactorymanages reporter lifecycle and aggregatesRunResultacross forks
When multiple forks run in parallel, each fork has its own DefaultReporterFactory. Results are merged by ReportsMerger into a single aggregate RunResult that determines the build status.
The maven-surefire-report-plugin reads the XML files (via surefire-report-parser) and generates HTML reports — it operates post-build and is independent of the execution pipeline.
Group / Category Filtering
Since 3.6.0, test filtering by groups, categories, or tags is handled via the JUnit Platform's native tag expression syntax.
How it works
- Users configure
<groups>and<excludedGroups>in the plugin configuration - For JUnit 4:
@Categoryannotations are mapped to JUnit Platform tags via the Vintage Engine - For TestNG: groups are mapped to JUnit Platform tags via the TestNG Engine
- For JUnit 5:
@Tagannotations are used natively
Tag filter expressions are passed directly to the JUnit Platform Launcher.
Build and Test
# Full build with unit tests
mvn clean install
# Full build with integration tests
mvn clean install -P run-its
# Build a single module
mvn clean install -pl surefire-booter
# Run a single test
mvn test -pl surefire-booter -Dtest=ForkedBooterTest
# Run a single integration test
mvn verify -pl surefire-its -Prun-its -Dit.test=JUnit47RedirectOutputIT
# IDE setup (required before importing)
mvn install -P ide-development -f surefire-shared-utils/pom.xml
mvn compile -f surefire-grouper/pom.xml
Requirements: Maven 3.6.3+, JDK 8+ (source level 8, animal-sniffer enforces Java 8 API).
Stack trace filtering
A new StackTraceProvider optimizes memory usage by truncating stack traces to 15 frames and filtering JDK packages by default. This is configurable:
<configuration>
<stackTraceFilterPrefixes>
<prefix>org.springframework.</prefix>
<prefix>org.junit.</prefix>
</stackTraceFilterPrefixes>
</configuration>
When not specified or empty, the default filters (java., javax., sun., jdk.) apply.
Stack Trace Memory Optimization
Problem
To associate console output with test classes, Surefire captures stack traces for every output line. With full stack traces (25–30 frames typical), this consumed 600–1,800 bytes per line.
Solution
The new StackTraceProvider class (in surefire-api) introduces:
- Frame limit: Maximum 15 frames per stack trace (sufficient to capture the test class after surefire framework frames)
- Package filtering: JDK packages (
java.,javax.,sun.,jdk.) filtered by default - Configurable prefixes: Users can specify custom filter prefixes that replace (not add to) the defaults
Memory impact
| Metric | Before | After |
|---|---|---|
| Frames per trace | 25–30 | ≤15 |
| Bytes per output line | 600–1,800 | 300–600 |
| Estimated savings | — | ~50% |
Configuration
<!-- Custom prefixes (replaces defaults) -->
<configuration>
<stackTraceFilterPrefixes>
<prefix>org.springframework.</prefix>
<prefix>org.junit.</prefix>
</stackTraceFilterPrefixes>
</configuration>
# Command line
mvn test -Dsurefire.stackTraceFilterPrefixes=org.springframework.,org.junit.



