1. Introduction
Starting with JUnit 4, tests can be run in parallel to gain speed for larger suites. The problem was concurrent test execution was not fully supported by the Spring TestContext Framework prior to Spring 5.
In this quick article, we’ll show how to use Spring 5 to run our tests in Spring projects concurrently.
2. Maven Setup
As a reminder, to run JUnit tests in parallel, we need to configure the maven-surefire-plugin to enable the feature:
<build> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <configuration> <parallel>methods</parallel> <useUnlimitedThreads>true</useUnlimitedThreads> </configuration> </plugin> </build>
You can check out the reference documentation for a more detailed configuration on parallel test execution.
3. Concurrent Test
The following example test would fail when running in parallel for versions prior to Spring 5.
However, it will run smoothly in Spring 5:
@RunWith(SpringRunner.class) @ContextConfiguration(classes = Spring5JUnit4ConcurrentTest.SimpleConfiguration.class) public class Spring5JUnit4ConcurrentTest implements ApplicationContextAware, InitializingBean { @Configuration public static class SimpleConfiguration {} private ApplicationContext applicationContext; private boolean beanInitialized = false; @Override public void afterPropertiesSet() throws Exception { this.beanInitialized = true; } @Override public void setApplicationContext( final ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Test public void whenTestStarted_thenContextSet() throws Exception { TimeUnit.SECONDS.sleep(2); assertNotNull( "The application context should have been set due to ApplicationContextAware semantics.", this.applicationContext); } @Test public void whenTestStarted_thenBeanInitialized() throws Exception { TimeUnit.SECONDS.sleep(2); assertTrue( "This test bean should have been initialized due to InitializingBean semantics.", this.beanInitialized); } }
When running sequentially, the tests above would take around 6 seconds to pass. With concurrent execution, it will only take about 4.5 seconds – which is quite typical for how much time we can expect to save in larger suites as well.
4. Under the Hood
The primary reason prior versions of the framework didn’t support running tests concurrently was due to the management of TestContext by the TestContextManager.
In Spring 5, the TestContextManager uses a thread local – TestContext – to ensure that operations on TestContexts in each thread would not interfere with each other. Thus thread-safety is guaranteed for most method level and class level concurrent tests:
public class TestContextManager { // ... private final TestContext testContext; private final ThreadLocal<TestContext> testContextHolder = new ThreadLocal<TestContext>() { protected TestContext initialValue() { return copyTestContext(TestContextManager.this.testContext); } }; public final TestContext getTestContext() { return this.testContextHolder.get(); } // ... }
Note that the concurrency support does not apply to all kinds of tests; we need to exclude tests that:
- change external shared states, such as states in caches, databases, message queues, etc.
- require specific execution orders, for example, tests that use JUnit‘s @FixMethodOrder
- modify the ApplicationContext, which are generally marked by @DirtiesContext
5. Summary
In this quick tutorial, we’ve shown an basic example using Spring 5 to run tests in parallel.
As always, the example code can be found over on Github.