Running tests sequentially is safe but slow. A test suite that takes 10 minutes to run sequentially can often complete in 2–3 minutes with parallel execution. JUnit 6 has first-class support for parallel test execution — but it requires careful configuration to avoid flaky tests caused by shared state. This guide covers every configuration option, pitfall, and real-world pattern you need to enable parallel execution confidently.
How JUnit 6 Parallel Execution Works
JUnit 6 parallel execution is disabled by default. When enabled, JUnit can run tests at two levels:
- Class-level parallelism — multiple test classes run concurrently in different threads
- Method-level parallelism — multiple test methods within the same class run concurrently
These are configured independently, giving you fine-grained control over the degree of parallelism.
Step 1: Enable Parallel Execution
Create or edit src/test/resources/junit-platform.properties:
# Enable parallel execution (disabled by default)
junit.jupiter.execution.parallel.enabled=true
# Default execution mode for all tests
# 'concurrent' = run in parallel with other tests at the same level
# 'same_thread' = run in the same thread as parent (sequential)
junit.jupiter.execution.parallel.mode.default=concurrent
# Default execution mode for top-level test classes
# concurrent = classes run in parallel
# same_thread = classes run sequentially (but methods within might be concurrent)
junit.jupiter.execution.parallel.mode.classes.default=concurrent
Step 2: Configure the Thread Pool Strategy
# Strategy: 'dynamic' uses (availableProcessors * factor) threads
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0
# Strategy: 'fixed' uses an exact thread count
# junit.jupiter.execution.parallel.config.strategy=fixed
# junit.jupiter.execution.parallel.config.fixed.parallelism=4
# Strategy: 'custom' uses your own ParallelExecutionConfigurationStrategy
# junit.jupiter.execution.parallel.config.strategy=custom
# junit.jupiter.execution.parallel.config.custom.class=com.example.MyStrategy
Step 3: Control Parallelism Per Test with @Execution
Override the global setting for specific classes or methods using the @Execution annotation:
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
// This class runs its methods CONCURRENTLY (parallel within the class)
@Execution(ExecutionMode.CONCURRENT)
class FastStatelessCalculatorTest {
// Both methods run in parallel — safe because Calculator is stateless
@Test
void additionTest() {
assertEquals(5, new Calculator().add(2, 3));
}
@Test
void multiplicationTest() {
assertEquals(6, new Calculator().multiply(2, 3));
}
}
// This class forces sequential execution even if global mode is concurrent
// Use for tests that share state or external resources
@Execution(ExecutionMode.SAME_THREAD)
class StatefulDatabaseTest {
// Methods run one after another — safe because they share DB state
@Test
void insertUser() { /* inserts a row */ }
@Test
void queryUser() { /* reads the row inserted above */ }
}
Step 4: Synchronise Shared Resources with @ResourceLock
@ResourceLock prevents concurrent access to a named shared resource. Tests annotated with the same resource name will not run concurrently with each other:
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
class SystemPropertyTest {
// READ lock: multiple readers can run concurrently
@Test
@ResourceLock(value = "system.properties", mode = ResourceAccessMode.READ)
void readingSystemPropertyIsThreadSafe() {
assertNotNull(System.getProperty("user.home"));
}
// READ_WRITE lock: exclusive access — no other test using this resource runs concurrently
@Test
@ResourceLock(value = "system.properties", mode = ResourceAccessMode.READ_WRITE)
void settingSystemPropertyAffectsGlobalState() {
String originalValue = System.getProperty("my.test.flag");
try {
System.setProperty("my.test.flag", "true");
assertEquals("true", System.getProperty("my.test.flag"));
} finally {
// Always restore system property to avoid polluting other tests
if (originalValue == null) {
System.clearProperty("my.test.flag");
} else {
System.setProperty("my.test.flag", originalValue);
}
}
}
}
Complete Configuration Example
# src/test/resources/junit-platform.properties
# Enable parallel execution
junit.jupiter.execution.parallel.enabled=true
# Run test CLASSES concurrently
junit.jupiter.execution.parallel.mode.classes.default=concurrent
# Run test METHODS sequentially within each class (safer default)
junit.jupiter.execution.parallel.mode.default=same_thread
# Use dynamic thread count: (CPU cores * 1.5)
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.5
# Safety net: abort any test taking longer than 60 seconds
junit.jupiter.execution.timeout.default=60s
Common Pitfalls and How to Fix Them
| Problem | Root Cause | Fix |
|---|---|---|
| Tests pass alone but fail in parallel | Shared mutable static state | Remove static state; use instance fields with @BeforeEach |
| Database constraint violations | Two tests insert same unique key | Use unique test data per test; use @ResourceLock |
| System property pollution | One test sets a property, another reads it | Use @ResourceLock with READ_WRITE mode |
| Port conflicts in server tests | Two tests start server on same port | Use random port selection; Testcontainers handles this automatically |
| ThreadLocal data leaks | ThreadLocal not cleared after test | Clear ThreadLocal in @AfterEach |
| Flaky ordering-dependent tests | Tests rely on execution order | Make each test independent; use @BeforeEach for fresh state |
Measuring the Speed-Up
# Run tests sequentially (baseline)
mvn test -Djunit.jupiter.execution.parallel.enabled=false
# [INFO] Tests run: 120, Time elapsed: 45.2 s
# Run tests in parallel (concurrent classes, same-thread methods)
mvn test -Djunit.jupiter.execution.parallel.enabled=true
# [INFO] Tests run: 120, Time elapsed: 12.8 s
# Speed-up: 45.2 / 12.8 = 3.5x faster on an 8-core machine
Frequently Asked Questions (FAQs)
Q1: Is it safe to enable parallel execution on existing test suites?
Start conservatively: enable class-level parallelism only (mode.classes.default=concurrent, mode.default=same_thread). This runs classes concurrently but methods within each class sequentially. Most test suites are safe at this level. Once tests pass, gradually enable method-level parallelism for classes you are confident are stateless. Use @Execution(SAME_THREAD) on any class that shares external state.
Q2: How many threads should I use?
For CPU-bound tests (pure computation), set the thread count to match your available processors. For I/O-bound tests (database, HTTP calls), a higher count (2–3x processors) is often faster because threads spend time waiting. The dynamic strategy with a factor between 1.0 and 2.0 is a good starting point for mixed workloads. Profile your specific suite to find the optimal value.
Q3: Does parallel execution work with Spring Boot tests?
Yes, but with care. Spring Boot’s ApplicationContext caching means multiple parallel test classes may share the same context. This is generally thread-safe for read-only tests but can cause issues if tests modify the context (e.g., using @MockBean differently per class). Use @Execution(SAME_THREAD) on classes that modify Spring beans. See JUnit 6 with Spring Boot for detailed guidance.
Q4: How do I disable parallel execution for a single test class?
Annotate the class with @Execution(ExecutionMode.SAME_THREAD). This forces all methods in that class to run sequentially in the same thread, regardless of the global configuration. This is the recommended approach for tests that use shared external resources, system properties, or static fields.
Q5: Can I use parallel execution with @TestFactory dynamic tests?
Yes. @TestFactory methods participate in parallel execution at the method level. The generated DynamicTest instances run in the same thread as the factory method by default. To make individual dynamic tests run in parallel, the factory method itself must be marked @Execution(CONCURRENT) and each dynamic test’s executable will be scheduled on the parallel executor.
See Also
- JUnit 6 Tutorial: Complete Series Index
- Tags and Test Suites in JUnit 6: Organizing Large Test Bases
- Running JUnit 6 Tests in CI/CD Pipelines
- How to Fix Flaky Tests in JUnit 6
- JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing
Conclusion
Parallel test execution is one of the highest-return configuration changes you can make to a mature JUnit 6 project. Start with class-level concurrency, guard shared resources with @ResourceLock, and use @Execution(SAME_THREAD) to opt individual classes out. Done correctly, you can cut a 10-minute test suite down to 2–3 minutes without changing a single test assertion.
Next: JUnit 6 Internals: How the Test Engine Works — understand the engine lifecycle, test descriptor trees, and how to use the Launcher API programmatically.