skip to content

As a tech lead, how would you design a fast, reliable Surefire test-execution strategy across a large multi-module build?

level: principalimportance: nice to knowfreq 28%

answer

  1. Surefire units / Failsafe ITs
  2. forkCount 1C + per-fork dirs/ports
  3. pin Surefire+JUnit in parent POM
  4. ban shared static state & order deps
  5. CI never skips; profiles for local

basics

~20 s

Split fast unit tests (Surefire) from slow integration tests (Failsafe), parallelize units with forkCount per core while keeping per-fork resource isolation, pin one Surefire and test-framework version via parent POM, and enforce deterministic, thread-safe tests so the suite stays both fast and reliable.

solid answer

~40 s

I'd separate concerns: Surefire runs fast, isolated unit tests in the test phase; Failsafe runs integration tests in integration-test/verify so cleanup always happens. For speed I'd parallelize units with forkCount=1C (or higher) and reuseForks=true to amortize JVM startup, giving each fork its own ports/temp dirs via ${surefire.forkNumber} to avoid clashes. Reliability comes from governance: pin Surefire and the JUnit/TestNG stack centrally in the parent POM (BOM-managed) so modules don't drift; ban shared mutable static state and order-dependence; quarantine flaky tests behind tags rather than disabling silently. I'd wire CI to consume surefire/failsafe XML uniformly, run units on every push and integration on a gated stage, and use profiles (e.g. -P fast) for local iteration. Treat -Dmaven.test.skip as an audited emergency lever only, never a default.

code

xml · 13 lines
xml
<build><pluginManagement><plugins>
  <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
      <forkCount>1C</forkCount>
      <reuseForks>true</reuseForks>
      <systemPropertyVariables>
        <test.tmp>${project.build.directory}/fork-${surefire.forkNumber}</test.tmp>
      </systemPropertyVariables>
    </configuration>
  </plugin>
</plugins></pluginManagement></build>

go deeper

for a junior

Understand units and integration tests are run by different plugins.

for a middle

Configure forkCount/reuseForks and separate unit vs integration phases.

for a senior

Balance parallel speed against isolation and diagnose resource-collision flakiness.

for a principal

Own the org-wide strategy: pluginManagement, version pinning, flaky-test policy, CI staging, and skip-flag governance.

## Goals A large multi-module build needs the test suite to be **fast enough** to run on every change yet **reliable enough** to trust. These pull in opposite directions (parallelism vs isolation), so the design is about managing that tension with governance. ## 1. Split unit vs integration - **Surefire** -> unit tests in `test` (fast, no external deps). - **Failsafe** -> integration tests in `integration-test`/`verify`, with `pre-/post-integration-test` for spinning resources up/down. Failure is deferred to `verify` so teardown always runs. - Adopt a naming/tag convention (`*Test` vs `*IT`, or `@Tag('integration')`). ## 2. Parallelize units safely ```xml <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> <configuration> <forkCount>1C</forkCount> <reuseForks>true</reuseForks> <systemPropertyVariables> <test.tmp>${project.build.directory}/fork-${surefire.forkNumber}</test.tmp> <server.port>0</server.port> <!-- random port --> </systemPropertyVariables> </configuration> </plugin> ``` `${surefire.forkNumber}` gives each JVM its own scratch space; random ports prevent collisions. Cap forks so CI agents don't thrash. ## 3. Governance for reliability - **Pin versions centrally**: one Surefire version and a BOM-managed JUnit/TestNG version in the parent POM, so 30 modules don't each drift and produce 'zero tests run' surprises. - **Enforce determinism**: no reliance on test order, no shared mutable statics/singletons across tests; fixed clocks/seeds. - **Flaky-test policy**: tag and quarantine, track, and fix - don't `@Disabled` silently. - **Skip discipline**: CI never skips; `-DskipTests` only for local artifact builds; `-Dmaven.test.skip` is an audited emergency path. ## 4. CI wiring - Run Surefire units on every push; gate Failsafe integration in a later stage (containers/DB). - Publish `target/surefire-reports` + `failsafe-reports` XML uniformly to the CI test dashboard. - Provide a `-P fast` profile for local loops (e.g. skip slow tags) without weakening CI. ## 5. Multi-module specifics - Use `mvn -T 1C` (build-level parallelism) carefully alongside Surefire fork parallelism to avoid oversubscribing cores. - Keep test configuration in the parent's `<pluginManagement>` so every module inherits the same isolation/parallelism settings. The net: parallelism for speed, per-fork isolation + centralized version pinning + determinism rules for reliability.

  • How do you stop parallel forks from colliding on ports and files?
    Give each fork its own resources using ${surefire.forkNumber} for temp dirs and bind to random/ephemeral ports (port 0).
  • Why centralize Surefire and JUnit versions in the parent POM?
    To prevent per-module drift that causes inconsistent or silently-skipped test runs across a large reactor.
  • How do you combine mvn -T (parallel modules) with Surefire forking?
    Carefully - both consume cores; size -T and forkCount together to avoid oversubscription and CPU thrash.

It's like running many restaurant kitchens (forks) in parallel: each needs its own pantry and stove (per-fork temp dirs/ports), one corporate recipe book (pinned versions), and a rule against cooks sharing the same cutting board (no shared state) - otherwise speed creates chaos.

saying these in an interview costs you the question

  • Making -DskipTests or maven.test.skip a default in CI.
  • Cranking up parallelism without per-fork resource isolation.
  • Letting each module pick its own Surefire/JUnit version.
  • Silently @Disabling flaky tests instead of quarantining and fixing.

context