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_threadconcurrent),我们有四种不同的执行模式:

  1. (same_thread, same_thread) - 所有测试顺序运行
  2. (same_thread, concurrent) - 一个类中的测试顺序运行,但多个类并行运行
  3. (concurrent, same_thread) - 一个类中的测试并行运行,但每个类单独运行
  4. (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上找到。


« 上一篇: Java Weekly, 第408期
» 下一篇: Evrete 规则引擎