1. 引言
在这篇文章中,我们将探讨如何使用JUnit 5执行并行单元测试。首先,我们会介绍基本配置和启用这个功能所需的最低要求。接下来,我们会展示不同情况下的代码示例,最后讨论共享资源的同步问题。
并行测试执行是自版本5.3起作为可选功能提供的实验性特性。
2. 配置
首先,我们需要在src/test/resources
文件夹中创建一个junit-platform.properties
文件来启用并行测试执行。我们通过在上述文件中添加以下行来开启并行化功能:
junit.jupiter.execution.parallel.enabled = true
让我们通过运行一些测试来检查我们的配置。首先,我们创建FirstParallelUnitTest
类,并在其中添加两个测试:
public class FirstParallelUnitTest{
@Test
public void first() throws Exception{
System.out.println("FirstParallelUnitTest first() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("FirstParallelUnitTest first() end => " + Thread.currentThread().getName());
}
@Test
public void second() throws Exception{
System.out.println("FirstParallelUnitTest second() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("FirstParallelUnitTest second() end => " + Thread.currentThread().getName());
}
}
当我们运行测试时,控制台会输出如下内容:
FirstParallelUnitTest second() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest second() end => ForkJoinPool-1-worker-19
FirstParallelUnitTest first() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest first() end => ForkJoinPool-1-worker-19
从输出中,我们可以注意到两点:首先,我们的测试按顺序运行;其次,我们使用了ForkJoin线程池。通过启用并行执行,JUnit引擎开始使用ForkJoin线程池。
接下来,我们需要添加配置以利用这个线程池。我们需要选择一个并行化策略。JUnit提供了两种实现(动态和固定)以及自定义选项来创建我们自己的实现。
动态策略根据处理器/核心数量乘以因子参数(默认为1)确定线程数,可以通过以下方式指定:
junit.jupiter.execution.parallel.config.dynamic.factor
另一方面,固定策略依赖于预定义的线程数,通过以下方式指定:
junit.jupiter.execution.parallel.config.fixed.parallelism
要使用自定义策略,我们需要首先创建它,通过实现ParallelExecutionConfigurationStrategy
接口。
3. 类内测试并行化
我们已经启用了并行执行并选择了策略。现在是时候在一个类内部并发地执行测试了。有两种方式来配置这个:一是使用@Execution(ExecutionMode.CONCURRENT)
注解,二是使用属性文件和行:
junit.jupiter.execution.parallel.mode.default = concurrent
在选择如何配置后,运行FirstParallelUnitTest
类,我们可以看到以下输出:
FirstParallelUnitTest second() start => ForkJoinPool-1-worker-5
FirstParallelUnitTest first() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest second() end => ForkJoinPool-1-worker-5
FirstParallelUnitTest first() end => ForkJoinPool-1-worker-19
从输出中,我们可以看到两个测试同时启动并在不同的线程中运行。请注意,每次运行的输出可能会有所不同。这是使用ForkJoin线程池时预期的结果。
还有一个选项是在FirstParallelUnitTest
类中的所有测试在同一线程中运行。在当前范围内,同时使用并行性和同一线程选项是不可行的,因此让我们在下一节扩展范围,并在下一部分添加另一个测试类。
4. 模块内测试并行化
在引入新属性之前,我们先创建一个名为SecondParallelUnitTest
的类,其中包含与FirstParallelUnitTest
相似的两个方法:
public class SecondParallelUnitTest{
@Test
public void first() throws Exception{
System.out.println("SecondParallelUnitTest first() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("SecondParallelUnitTest first() end => " + Thread.currentThread().getName());
}
@Test
public void second() throws Exception{
System.out.println("SecondParallelUnitTest second() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("SecondParallelUnitTest second() end => " + Thread.currentThread().getName());
}
}
在同时运行这两个测试类之前,我们需要设置属性:
junit.jupiter.execution.parallel.mode.classes.default = concurrent
当我们运行这两个测试类时,输出如下:
SecondParallelUnitTest second() start => ForkJoinPool-1-worker-23
FirstParallelUnitTest first() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest second() start => ForkJoinPool-1-worker-9
SecondParallelUnitTest first() start => ForkJoinPool-1-worker-5
FirstParallelUnitTest first() end => ForkJoinPool-1-worker-19
SecondParallelUnitTest first() end => ForkJoinPool-1-worker-5
FirstParallelUnitTest second() end => ForkJoinPool-1-worker-9
SecondParallelUnitTest second() end => ForkJoinPool-1-worker-23
从输出中,我们可以看到所有四个测试在不同的线程中并行运行。
结合本节和上一节提到的两个属性及其值(same_thread
和concurrent
),我们有四种不同的执行模式:
- (
same_thread
,same_thread
) - 所有测试顺序运行 - (
same_thread
,concurrent
) - 一个类中的测试顺序运行,但多个类并行运行 - (
concurrent
,same_thread
) - 一个类中的测试并行运行,但每个类单独运行 - (
concurrent
,concurrent
) - 测试并行运行
5. 同步
在理想情况下,所有的单元测试都是独立且隔离的。然而,有时很难实现,因为它们依赖于共享资源。在这种情况下,当并行运行测试时,我们需要在测试中同步共享资源。JUnit5为我们提供了通过@ResourceLock
注解的形式来实现这种机制。
同样,让我们创建一个名为ParallelResourceLockUnitTest
的类:
public class ParallelResourceLockUnitTest{
private List<String> resources;
@BeforeEach
void before() {
resources = new ArrayList<>();
resources.add("test");
}
@AfterEach
void after() {
resources.clear();
}
@Test
@ResourceLock(value = "resources")
public void first() throws Exception {
System.out.println("ParallelResourceLockUnitTest first() start => " + Thread.currentThread().getName());
resources.add("first");
System.out.println(resources);
Thread.sleep(500);
System.out.println("ParallelResourceLockUnitTest first() end => " + Thread.currentThread().getName());
}
@Test
@ResourceLock(value = "resources")
public void second() throws Exception {
System.out.println("ParallelResourceLockUnitTest second() start => " + Thread.currentThread().getName());
resources.add("second");
System.out.println(resources);
Thread.sleep(500);
System.out.println("ParallelResourceLockUnitTest second() end => " + Thread.currentThread().getName());
}
}
@ResourceLock
允许我们指定共享的资源以及想要使用的锁类型(默认为ResourceAccessMode.READ_WRITE
)。在当前设置下,JUnit引擎会检测到我们的测试都使用了共享资源,并将它们顺序执行:
ParallelResourceLockUnitTest second() start => ForkJoinPool-1-worker-5
[test, second]
ParallelResourceLockUnitTest second() end => ForkJoinPool-1-worker-5
ParallelResourceLockUnitTest first() start => ForkJoinPool-1-worker-19
[test, first]
ParallelResourceLockUnitTest first() end => ForkJoinPool-1-worker-19
6. 总结
在这篇文章中,我们首先介绍了如何配置并行执行。接下来,我们讨论了可用的并行化策略以及如何配置线程数。然后,我们讲解了不同配置如何影响测试执行。最后,我们讨论了共享资源的同步。
如往常一样,本文的代码可以在GitHub上找到。