Guides

Parallel execution

A seven-dimension assessment established exactly what holds under concurrency and what does not. Everything on this page is a measured result, not an expectation.

The short version

  • Sequential execution, the default, is fully supported.
  • Forked JVMs (forkCount > 1) are fully supported and are the recommended way to parallelise.
  • Parallel test classes are supported.
  • Parallel methods within one class are rejected before the first test runs.
  • Threads the test itself starts are supported; pooled threads it did not start are not.

Forked JVMs

Each fork gets its own JVM and therefore its own embedded store. There is no shared state to contend over, which is why forked JVMs are the parallelism this library supports for shortening a suite.

Shell
mvn verify -DforkCount=4

Parallel test classes

Each harness context prefixes its physical collection names with its own context id, so two classes running at once cannot see each other's data even when they use the same collection names.

src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4

One caveat worth setting expectations on: Spring test classes that share a cached application context are serialised against each other by design, because they share one harness context. Parallelism between such classes is zero regardless of the JUnit configuration. Classes with different context configurations do run concurrently.

The library deliberately ships no junit-platform.properties of its own. adding one to src/test/resources would change the default build for everyone.

Parallel methods: rejected

Cleanup runs after every test method and drops every database that method touched. With concurrent methods in one class sharing one context, the first method to finish destroys the data of every method still running. There is no configuration that makes this work, so rather than leaving it as a footgun the harness detects the configuration and fails fast with an explanatory error. Both harnesses use the same detector, so the message is identical either way.

Java
// This configuration is rejected before the first test runs
junit.jupiter.execution.parallel.mode.default=concurrent

// So is this, on a class using either harness
@Execution(ExecutionMode.CONCURRENT)
@EnableMongoEmulator
class OrderIT {
    @Test void a() { }
    @Test void b() { }
}

Threads your test starts

The active context is bound to the thread, and the binding is inheritable. Therefore, a thread the test creates sees the right context. A thread from a pool the test did not create may have been warmed while a different context was bound. Ambient resolution works only when exactly one context is open; stale or ambiguous bindings raise IllegalStateException, never a silent cross-test read. Hand the resolved client or collection to the worker instead.

Java
// Supported: the thread inherits the test's binding
Thread worker = new Thread(() -> MongoTestData.insert("orders", doc));
worker.start();
worker.join();

// Ambient resolution on a pooled thread works only when exactly one context is open
CompletableFuture.supplyAsync(() -> MongoTestSupport.collection("orders").countDocuments());

// Supported: hand the client to the worker instead of resolving it there
MongoCollection<Document> orders = MongoTestSupport.collection("orders");
CompletableFuture.supplyAsync(orders::countDocuments);

Documented as unsupported

  • Relying on ambient static-helper resolution from pooled threads when more than one context is open. Pass the client or collection explicitly.
  • Expecting parallelism between Spring classes that share a cached application context.
  • Transaction throughput scaling for commits that overlap the same physical collection. Commit locking is per physical collection: overlapping commits serialise, while commits touching disjoint collections run concurrently.
  • @Nested classes as isolation boundaries. During sequential execution they reuse the enclosing class's context.

Under parallel classes, the Spring harness refuses a nested class before mutating state when its enclosing class holds the shared context on another worker thread. Put @Execution(SAME_THREAD) on the nested class, or make it a top-level class.

A JUnit subtlety

@Execution(CONCURRENT) on a test class does make its methods concurrent: Jupiter resolves the class annotation as the ancestor mode those methods inherit. The extension therefore refuses it, including when it is inherited or declared on an enclosing class. Use junit.jupiter.execution.parallel.mode.classes.default=concurrent for parallel classes; a nearest-ancestor @Execution(SAME_THREAD) opts a class back out.