Fork me on GitHub

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 via maven-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 SurefireDependencyResolver and loaded in the forked JVM's classpath.
  • surefire-shared-utils shades commons-lang3, commons-io, commons-compress, and maven-shared-utils into org.apache.maven.surefire.shared.* to prevent version conflicts with user projects.
  • surefire-shadefire shades the entire surefire-junit-platform provider into org.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:

  1. 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>
    
  2. 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:

  1. Provider generates ReportEntry events (test started, succeeded, failed, etc.)
  2. TestSetRunListener receives events and delegates to registered reporters
  3. StatelessXmlReporter writes TEST-*.xml files (JUnit/Ant-compatible format)
  4. ConsoleOutputFileReporter writes *-output.txt files with captured stdout/stderr
  5. DefaultReporterFactory manages reporter lifecycle and aggregates RunResult across 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

  1. Users configure <groups> and <excludedGroups> in the plugin configuration
  2. For JUnit 4: @Category annotations are mapped to JUnit Platform tags via the Vintage Engine
  3. For TestNG: groups are mapped to JUnit Platform tags via the TestNG Engine
  4. For JUnit 5: @Tag annotations 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.