跳到主要内容

JUnit 5 并行测试

· 阅读需 4 分钟
Jcyuyi
Learn, practice and build

准备测试样例

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());
}
}

默认执行结果

[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

启用 JUnit5 并行测试

gradle.build 中添加 junit.jupiter.execution.parallel System Property:

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")
}

执行结果

[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

可以看到,测试 Class 并行执行,并且每个 Class 中的 @Test 方法也同样并行执行了。

仅并行测试 Class

可以仅并行测试 Class,每个 Class 中的 @Test 方法保留单线程顺序执行。

gradle.build 中修改

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

执行结果

[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

可以看到,测试 Class 并行执行,同时 A1 A2 在 worker-3 中,B1 B2 在 worker-1 中分别单线程顺序执行。

设置固定值的并行度

默认 strategy 会根据 CPU 内核数量调整并行度。

gradle.build 中添加

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

可将并行度设置为固定值。

常见多线程环境测试问题

有时会遇到执行单独一个测试没问题,所有测试同时执行报错的情况。可以将 parallelism 设为 1 排查多线程环境问题。

常见的多线程环境测试问题是共享资源导致的,包括:

  • 共享同一个数据库,使用同名的 Table,View,Lock 等等
  • 共享同一个 static 变量,共享变量非线程安全
  • 共享同一个目录/文件

常见解决方案

  • 不同测试类使用不同的数据库,如果需要共享则给使用的资源添加前后缀名称
  • 使用 ResourceLock 给资源冲突的测试加锁
  • 尽量减少 static 变量的使用,使用线程安全的共享变量/单例设计模式,使用 ThreadLocal
  • 每个测试使用单独的 @TempDir

参考

Spring @Qualifier 分组依赖注入集合

· 阅读需 2 分钟
Jcyuyi
Learn, practice and build

使用 Spring 的集合类依赖注入时,有时需要对多个相同类型的 Bean 进行分组。 可以使用 @Qualifier 或者自定义 Qualifier Annotation 来实现。

@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);
}
}
}

输出:

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

自定义 Qualifier Annotation

另一种写法是使用自定义 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 {
}

  • @Qualifier(MY_QUALIFIER_A) 替换成 @QualifierA
  • @Qualifier(MY_QUALIFIER_B) 替换成 @QualifierB

即可达到同样的效果。