skip to content

After enabling parallel test execution with `maxParallelForks`, previously-green tests start failing intermittently. What's likely happening and how do you address it?

level: middleimportance: must knowfreq 48%

answer

  1. parallel forks expose hidden coupling
  2. fixed ports → use ephemeral (0)
  3. shared DB/schema → per-fork isolation
  4. shared temp paths → @TempDir
  5. confirm via maxParallelForks=1 / forkEvery=1
  6. isolate stateful subset into serial Test task

basics

~20 s

Parallel forks expose hidden shared state — fixed ports, a single shared DB/schema, common temp files, or order dependence between test classes. Make each fork self-contained: random ports, per-fork resources, @TempDir, and remove cross-class ordering assumptions.

solid answer

~50 s

Raising `maxParallelForks` runs test *classes* in separate JVMs concurrently, which surfaces coupling that was masked by sequential execution. Typical culprits: - **Fixed network ports** — two forks bind the same port → bind exceptions. Use `0` (ephemeral) ports. - **Shared database / schema** — concurrent classes read/write the same rows or create the same table. Give each fork an isolated schema/DB (Testcontainers per fork, or a fork-indexed schema). - **Shared filesystem paths** — a hard-coded temp dir collides. Use JUnit 5 `@TempDir` or unique paths. - **Static / global mutable state** — system properties, default timezone, singletons mutated by one class observed by another. - **Order dependence** — tests that relied on running after some other class. Diagnose by setting `forkEvery = 1` or `maxParallelForks = 1` to confirm isolation fixes it, then fix the root cause. If a subset is irreducibly stateful, split it into its own sequential Test task rather than serialising the whole suite.

code

kotlin · 10 lines
kotlin
// Keep the bulk parallel; isolate the stateful subset.
val serialTests by tasks.registering(Test::class) {
    maxParallelForks = 1
    useJUnitPlatform { includeTags("serial") }
}
tasks.test {
    maxParallelForks = 4
    useJUnitPlatform { excludeTags("serial") }
}
tasks.check { dependsOn(serialTests) }

go deeper

for a junior

Recognise that parallel runs can flake due to shared ports/files; know to try maxParallelForks = 1 to check.

for a middle

Enumerate the common shared-state hazards (ports, DB, temp files, statics, ordering) and the per-fork isolation fixes; know the diagnostic workflow.

for a senior

Design isolation (random ports, per-fork DB/schema, tagging a serial subset) and drive root-cause fixes rather than serialising everything.

for a principal

Establish test-isolation standards and CI conventions so suites stay parallel-safe as the codebase and team grow.

## Why green tests suddenly flake Sequential execution (`maxParallelForks = 1`) accidentally guarantees that no two test classes touch a shared resource at the same time, and it fixes an ordering. Bump `maxParallelForks` and Gradle runs different classes in **separate JVM processes simultaneously** — that accidental guarantee is gone, and latent coupling becomes real failures. Note the granularity: forks isolate **classes** from each other in separate processes. So the new contention is *between classes* sharing something external to the JVM. ## The usual suspects ### Fixed ports Two forks each start a server on port 8080 → `Address already in use`. ```kotlin // instead of a hard-coded port val server = HttpServer.create(InetSocketAddress(0), 0) // 0 = OS-assigned val actualPort = server.address.port ``` In Spring: `@SpringBootTest(webEnvironment = RANDOM_PORT)` and inject `@LocalServerPort`. ### Shared database / schema Concurrent classes inserting into the same table or running migrations on a shared DB clobber each other. Options: a fresh Testcontainers database per fork, or derive a per-fork schema name from the worker, e.g. an env var Gradle can pass: ```kotlin tasks.test { maxParallelForks = 4 // each worker can read this to pick an isolated schema/db environment("TEST_DB_SCHEMA", "test_") } ``` (Workers expose an index you can incorporate so each fork is isolated.) ### Shared filesystem Hard-coded `/tmp/myapp-test` collides across forks. Use `@TempDir` (JUnit 5) which gives a unique directory per test. ### Global mutable JVM state `System.setProperty`, `TimeZone.setDefault`, `Locale.setDefault`, or a mutated singleton: one class leaves the JVM dirty and a class later in the *same* fork misbehaves. Clean up in `@AfterEach`/`@AfterAll`, or use `forkEvery` to recycle the JVM. ### Order dependence A test that only passes when another class ran first is broken regardless of parallelism; parallelism just exposes it. Make each test set up its own preconditions. ## Diagnostic workflow 1. Reproduce reliably: run the suite a few times; note which classes flake together. 2. Prove it's isolation: temporarily set `maxParallelForks = 1` (or `forkEvery = 1`). If flakiness vanishes, it's shared state. 3. Identify the shared resource (port, DB, file, static). 4. Fix at the root — isolate the resource per fork/test. 5. Restore parallelism and re-verify. ## When you can't fix it now Don't serialise the *entire* suite for one bad subset. Carve the stateful tests into a dedicated Test task with `maxParallelForks = 1`, and keep the rest parallel: ```kotlin val serialTests by tasks.registering(Test::class) { maxParallelForks = 1 useJUnitPlatform { includeTags("serial") } } tasks.test { useJUnitPlatform { excludeTags("serial") } } ```

  • How do you confirm the flakiness is caused by parallelism rather than a real bug?
    Temporarily set maxParallelForks = 1 (and optionally forkEvery = 1). If the tests go green, the problem is shared state/ordering surfaced by concurrency, not a logic bug in the code under test.
  • How do you keep most tests parallel when only a few classes are irreducibly stateful?
    Tag the stateful tests and route them to a dedicated Test task with maxParallelForks = 1, while the main test task runs the rest in parallel and excludes that tag.
  • Two forks fail with 'Address already in use'. Fix?
    Stop hard-coding the port; bind to port 0 (OS-assigned ephemeral) and read back the actual port — e.g. Spring's RANDOM_PORT web environment with @LocalServerPort.

saying these in an interview costs you the question

  • Concluding the production code is buggy when it's a test-isolation problem.
  • Disabling parallelism for the whole suite instead of isolating the offenders.
  • Assuming forks share a JVM/static state — they're separate processes; the contention is over *external* shared resources (and within a fork across classes).

context