Apache Maven Surefire — Architecture Overview
Architecture reference for the
masterbranch (version 3.5.x). For the upcoming 3.6.0 changes, see PR #3179 — Unified JUnit Platform Provider.
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 3, JUnit 4, JUnit 5 (Jupiter), TestNG, and plain POJO tests — each via a dedicated provider module. 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>JUnit 5 / Jupiter</i>"]
J4["surefire-junit4"]
J47["surefire-junit47<br/><i>Parallel + categories</i>"]
J3["surefire-junit3<br/><i>JUnit 3 + POJO</i>"]
TNG["surefire-testng"]
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
subgraph "Filtering"
SG["surefire-grouper<br/><i>JavaCC category expression parser</i>"]
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
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
| 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 |
Auto-detection
When no provider is manually configured, Surefire scans the test classpath and selects the first applicable provider:
flowchart TD
Start["Scan test classpath"] --> SPI{"SPI configured?<br/>(META-INF/services)"}
SPI -->|Yes| UseSPI["Use SPI provider(s)"]
SPI -->|No| JP{"JUnit Platform<br/>on classpath?"}
JP -->|Yes| UseJP["Use surefire-junit-platform"]
JP -->|No| TNG{"TestNG<br/>on classpath?"}
TNG -->|Yes| UseTNG["Use surefire-testng"]
TNG -->|No| J47{"JUnit ≥4.7 AND<br/>(parallel OR groups)?"}
J47 -->|Yes| UseJ47["Use surefire-junit47"]
J47 -->|No| J4{"JUnit 4.x<br/>on classpath?"}
J4 -->|Yes| UseJ4["Use surefire-junit4"]
J4 -->|No| J3["Use surefire-junit3<br/>(always applicable — fallback)"]
The priority order is defined in AbstractSurefireMojo.createProviders(). Surefire resolves the provider's dependencies at runtime via SurefireDependencyResolver and adds them to the forked JVM's classpath — the provider JAR is never a compile-time dependency of the 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
The surefire-grouper module provides test filtering by JUnit 4 categories or TestNG groups.
How it works
- Users configure
<groups>and<excludedGroups>in the plugin configuration - For JUnit 4.7+: the
surefire-junit47provider usesGroupMatcherCategoryFilterto filter tests by@Categoryannotations - For TestNG: groups are passed directly to TestNG's native group filtering
- For JUnit Platform: tag filter expressions are passed to the JUnit Platform Launcher
The module includes a JavaCC-generated parser (category-expression.jj) that parses boolean expressions over group names (e.g., "fast AND NOT slow"), producing a GroupMatcher tree (AndGroupMatcher, OrGroupMatcher, InverseGroupMatcher, SingleGroupMatcher).
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).



