Skip to main content

JUnit 5 parallel testing

· 4 min read
Jcyuyi
Learn, practice and build

Prepare test cases

public class TestClassBase {
private static final Logger log = LoggerFactory.getLogger(TestClassBase.class);

@BeforeEach
public void beforeEach(final TestInfo testInfo) {
log.info("Running {} in {}", testInfo.getDisplayName(), Thread.currentThread().getName());
}

@AfterEach
public void afterEach(final TestInfo testInfo) {
log.info("Finished {} in {}", testInfo.getDisplayName(), Thread.currentThread().getName());
}
}

public class TestClassA extends TestClassBase {
@ParameterizedTest(name = "test: {arguments}")
@ValueSource(strings = { "A1", "A2" })
public void timeConsumingTest(final String name) throws Exception {
Thread.sleep(Duration.ofSeconds(1).toMillis());
}
}

public class TestClassB extends TestClassBase {
@ParameterizedTest(name = "test: {arguments}")
@ValueSource(strings = { "B1", "B2" })
public void timeConsumingTest(final String name) throws Exception {
Thread.sleep(Duration.ofSeconds(1).toMillis());
}
}

Default output

[Test worker] INFO TestClassBase - Running test: A1 in Test worker
[Test worker] INFO TestClassBase - Finished test: A1 in Test worker
[Test worker] INFO TestClassBase - Running test: A2 in Test worker
[Test worker] INFO TestClassBase - Finished test: A2 in Test worker
[Test worker] INFO TestClassBase - Running test: B1 in Test worker
[Test worker] INFO TestClassBase - Finished test: B1 in Test worker
[Test worker] INFO TestClassBase - Running test: B2 in Test worker
[Test worker] INFO TestClassBase - Finished test: B2 in Test worker

Enable JUnit 5 parallel testing

Add junit.jupiter.execution.parallel System Properties to gradle.build:

test {
//...
systemProperty("junit.jupiter.execution.parallel.enabled", true)
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent")
}

Output

[ForkJoinPool-1-worker-5] INFO TestClassBase - Running test: A2 in ForkJoinPool-1-worker-5
[ForkJoinPool-1-worker-2] INFO TestClassBase - Running test: A1 in ForkJoinPool-1-worker-2
[ForkJoinPool-1-worker-6] INFO TestClassBase - Running test: B2 in ForkJoinPool-1-worker-6
[ForkJoinPool-1-worker-4] INFO TestClassBase - Running test: B1 in ForkJoinPool-1-worker-4
[ForkJoinPool-1-worker-5] INFO TestClassBase - Finished test: A2 in ForkJoinPool-1-worker-5
[ForkJoinPool-1-worker-6] INFO TestClassBase - Finished test: B2 in ForkJoinPool-1-worker-6
[ForkJoinPool-1-worker-2] INFO TestClassBase - Finished test: A1 in ForkJoinPool-1-worker-2
[ForkJoinPool-1-worker-4] INFO TestClassBase - Finished test: B1 in ForkJoinPool-1-worker-4

As you can see, the test classes are executed in parallel, and each @Test method within each class is also executed concurrently.

Class-level only parallel testing

You can parallelize the testing of classes while keeping the execution of @Test methods within each class in a single-threaded, sequential order.

Modify gradle.build:

test {
//...
systemProperty("junit.jupiter.execution.parallel.mode.default", "same_thread")
systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent")
}

Output

[ForkJoinPool-1-worker-3] INFO TestClassBase - Running test: A1 in ForkJoinPool-1-worker-3
[ForkJoinPool-1-worker-1] INFO TestClassBase - Running test: B1 in ForkJoinPool-1-worker-1
[ForkJoinPool-1-worker-3] INFO TestClassBase - Finished test: A1 in ForkJoinPool-1-worker-3
[ForkJoinPool-1-worker-1] INFO TestClassBase - Finished test: B1 in ForkJoinPool-1-worker-1
[ForkJoinPool-1-worker-1] INFO TestClassBase - Running test: B2 in ForkJoinPool-1-worker-1
[ForkJoinPool-1-worker-3] INFO TestClassBase - Running test: A2 in ForkJoinPool-1-worker-3
[ForkJoinPool-1-worker-1] INFO TestClassBase - Finished test: B2 in ForkJoinPool-1-worker-1
[ForkJoinPool-1-worker-3] INFO TestClassBase - Finished test: A2 in ForkJoinPool-1-worker-3

You can see that the test classes are executed in parallel. A1 and A2 run sequentially in worker-3, while B1 and B2 run sequentially in worker-1.

Set a Fixed Parallelism Value

By default, the strategy adjusts parallelism based on the number of CPU cores.

Add the following System Properties to gradle.build:

test {
//...
systemProperty("junit.jupiter.execution.parallel.config.strategy", "fixed")
systemProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", 4)
}

to set the parallelism to a fixed value.

Common Issues in Multi-threaded Environment Testing

Sometimes, a single test runs fine, but errors occur when all tests run simultaneously. Setting parallelism to 1 can help troubleshoot multi-threading issues.

Common multi-threaded environment testing problems include shared resources, such as:

  • Sharing the same database with tables, views, locks, etc.
  • Sharing the same static variable that isn't thread-safe
  • Sharing the same directory/file

Common Solutions

  • Different test classes should use different databases. If sharing is necessary, add prefixes or suffixes to the resource names being used.
  • Use ResourceLock to add locks to tests that have resource conflicts.
  • Minimize the use of static variables. Instead, use thread-safe shared variables, the singleton design pattern, or ThreadLocal.
  • Each test should use its own @TempDir

Reference

Use @Qualifier to inject collections groups in Spring

· One min read
Jcyuyi
Learn, practice and build

When using collection dependency injection in Spring, sometimes it's necessary to group multiple beans of the same type. This can be achieved by using either @Qualifier or custom qualifier annotations.

@Qualifier

@Configuration
public class QualifiedBeansConfig {
private static final Logger log = LoggerFactory.getLogger(QualifiedBeansConfig.class);

public static final String MY_QUALIFIER_A = "MY_QUALIFIER_A";
public static final String MY_QUALIFIER_B = "MY_QUALIFIER_B";

@Bean
@Qualifier(MY_QUALIFIER_A)
public MyBean myBean1() {
return new MyBean("1");
}

@Bean
@Qualifier(MY_QUALIFIER_A)
public MyBean myBean2() {
return new MyBean("2");
}

@Bean
@Qualifier(MY_QUALIFIER_B)
public MyBean myBean3() {
return new MyBean("3");
}

@Component
public static class BeanCollections {
public BeanCollections(final List<MyBean> myBeansList,
@Qualifier(MY_QUALIFIER_A) final Set<MyBean> myBeansSet,
@Qualifier(MY_QUALIFIER_B) final Map<String, MyBean> myBeansMap
) {
log.info("myBeansList: {}", myBeansList);
log.info("myBeansSet: {}", myBeansSet);
log.info("myBeansMap: {}", myBeansMap);
}
}
}

Output

...   : myBeansList: [MyBean[key=1], MyBean[key=2], MyBean[key=3]]
... : myBeansSet: [MyBean[key=1], MyBean[key=2]]
... : myBeansMap: {myBean3=MyBean[key=3]}

Custom Qualifier Annotation

Another approach is to use Custom Qualifier Annotation:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface QualifierA {
}

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface QualifierB {
}

Replace

  • @Qualifier(MY_QUALIFIER_A) to @QualifierA
  • @Qualifier(MY_QUALIFIER_B) to @QualifierB

to achieve the same goal.