Need help with your test setup? Reach out to us.
There are two main levels at which test parallelization can occur:
- JVM-Level Parallelization (Multiple Processes): Maven Surefire/Failsafe creates multiple JVM processes
- Thread-Level Parallelization (Single Process): JUnit Jupiter (JUnit 5) runs tests in parallel within a single JVM
Each approach has different benefits and trade-offs.
Remember that the right configuration depends on your specific testing needs, available hardware resources, and the nature of your tests.
Maven's testing plugins (Surefire for unit tests, Failsafe for integration tests) can create multiple JVM processes ("forks") to run tests in parallel.
- Each fork is a completely separate JVM process
- Full isolation of classloader, memory, and system properties
- Higher memory overhead (multiple JVMs)
- Strong isolation (good for tests that modify system properties or shared state)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<!-- Number of JVM forks to create (2C = 2 per CPU core) -->
<forkCount>2C</forkCount>
<!-- Whether to reuse JVM forks or create new ones for each test class -->
<reuseForks>true</reuseForks>
<!-- Memory settings per fork -->
<argLine>-Xmx1024m</argLine>
</configuration>
</plugin>The Failsafe plugin uses the same configuration parameters as Surefire but runs tests in the integration-test phase:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<forkCount>1C</forkCount>
<reuseForks>true</reuseForks>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>JUnit 5 (Jupiter) offers its own parallelization mechanism using threads within a single JVM.
- Uses a thread pool within a single JVM
- Shared memory space (more memory efficient)
- Lower startup overhead (no JVM creation)
- Configured via properties or annotations
Place a junit-platform.properties file in your src/test/resources directory:
# Enable parallel execution
junit.jupiter.execution.parallel.enabled = true
# Configure execution mode for classes and methods
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
# Configure thread pool (fixed size with 4 threads)
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4
# Maximum thread pool size (prevents excessive thread creation)
junit.jupiter.execution.parallel.config.fixed.max-pool-size = 8You can also configure parallelization using annotations on individual test classes:
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
@Execution(ExecutionMode.CONCURRENT)
public class MyParallelTest {
// Tests in this class will run in parallel
}You can combine both approaches for maximum parallelization:
- Maven Surefire/Failsafe creates multiple JVM forks
- Within each fork, JUnit Jupiter runs tests in parallel on multiple threads
This creates a two-level parallelization strategy.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<forkCount>2</forkCount>
<reuseForks>true</reuseForks>
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4
</configurationParameters>
</properties>
</configuration>
</plugin>When using databases in tests (especially with Testcontainers), you need to consider how parallelization affects database access:
- Each fork gets its own JVM with isolated system properties
- Perfect for Testcontainers with PostgreSQL, as each fork creates its own container
- No conflicts between system properties
- All threads in a JVM share system properties
- Can lead to conflicts when setting database connection properties
- Consider these strategies:
- Thread-local database connection properties
- Using Testcontainers JDBC URL with random database names
- Using dynamic database instances per test class
One simple solution is to use Testcontainers' JDBC URL format:
# In application-test.properties
spring.datasource.url=jdbc:tc:postgresql:14:///test_${random.uuid}
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriverThis creates a new container per thread without system property conflicts.