After enabling parallel test execution with `maxParallelForks`, previously-green tests start failing intermittently. What's likely happening and how do you address it?
answer
- parallel forks expose hidden coupling
- fixed ports → use ephemeral (0)
- shared DB/schema → per-fork isolation
- shared temp paths → @TempDir
- confirm via maxParallelForks=1 / forkEvery=1
- isolate stateful subset into serial Test task
basics
~20 sParallel 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 sRaising `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// 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
Recognise that parallel runs can flake due to shared ports/files; know to try maxParallelForks = 1 to check.
Enumerate the common shared-state hazards (ports, DB, temp files, statics, ordering) and the per-fork isolation fixes; know the diagnostic workflow.
Design isolation (random ports, per-fork DB/schema, tagging a serial subset) and drive root-cause fixes rather than serialising everything.
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).