Parallel Test Execution for JUnit 5 – JUnit 5的并行测试执行

最后修改: 2021年 10月 18日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this article, we’ll cover how to execute parallel unit tests using JUnit 5. First, we’ll cover basic configuration and minimal requirements to start using this feature. Next, we’ll show code examples for different situations, and in the end, we’ll talk about the synchronization of shared resources.

在本文中,我们将介绍如何使用JUnit 5执行并行单元测试。首先,我们将介绍基本配置和最低要求,以便开始使用这一功能。接下来,我们将展示不同情况下的代码示例,最后,我们将讨论共享资源的同步问题。

Parallel test execution is an experimental feature available as an opt-in since version 5.3.

平行测试执行是一个实验性的功能,从5.3版本开始可以选择加入。

2. Configuration

2.配置

First, we need to create a junit-platform.properties file in our src/test/resources folder to enable parallel test execution. We enable the parallelization feature by adding the following line in the mentioned file:

首先,我们需要在我们的src/test/resources文件夹中创建一个junit-platform.properties文件以启用并行测试执行。我们通过在所述文件中添加以下一行来启用并行化功能。

junit.jupiter.execution.parallel.enabled = true

Let’s check our configuration by running a few tests. First, we’ll create the FirstParallelUnitTest class and two tests in it:

让我们通过运行一些测试来检查我们的配置。首先,我们将创建 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());
    }
}

When we run our tests, we get the following output in the console:

当我们运行我们的测试时,我们在控制台得到以下输出。

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

In this output, we can notice two things. First, our tests run sequentially. Second, we use the ForkJoin thread pool. By enabling parallel execution, the JUnit engine starts using the ForkJoin thread pool.

在这个输出中,我们可以注意到两件事。首先,我们的测试是按顺序运行的。第二,我们使用了ForkJoin线程池。通过启用并行执行,JUnit 引擎开始使用 ForkJoin 线程池。

Next, we need to add a configuration to utilize this thread pool. We need to choose a parallelization strategy. JUnit provides two implementations (dynamic and fixed) and a custom option to create our implementation.

接下来,我们需要添加一个配置来利用这个线程池。我们需要选择一种并行化策略。JUnit提供了两种实现方式(动态固定)和自定义选项来创建我们的实现。

Dynamic strategy determines the number of threads  based on the number of processors/cores multiplied by factor parameter (defaults to 1) specified using:

动态策略根据处理器/核的数量乘以因子参数(默认为1)来确定线程的数量,使用。

junit.jupiter.execution.parallel.config.dynamic.factor

On the other hand, the fixed strategy relies on a predefined number of threads specified by:

另一方面,固定策略依赖于一个预定义的线程数量,由。

junit.jupiter.execution.parallel.config.fixed.parallelism

To use the custom strategy, we need to create it first by implementing the ParallelExecutionConfigurationStrategy interface.

为了使用自定义策略,我们需要首先通过实现ParallelExecutionConfigurationStrategy接口来创建它。

3. Test Parallelization Within a Class

3.测试类内的平行化

We already enabled parallel execution and picked a strategy. Now it’s time to execute tests in parallel within the same class. There are two ways to configure this. One is using @Execution(ExecutionMode.CONCURRENT) annotation, and the second is using properties file and line:

我们已经启用了并行执行并选择了一个策略。现在是时候在同一个类中并行执行测试了。有两种方法来配置这个。一种是使用@Execution(ExecutionMode.CONCURRENT) 注释,第二种是使用属性文件和行。

junit.jupiter.execution.parallel.mode.default = concurrent

After we choose how to configure this and run our FirstParallelUnitTest class, we can see the following output:

在我们选择如何配置并运行我们的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

From the output, we can see that both tests start simultaneously and in two different threads. Note that output can change from one run to another. This is expected when using the ForkJoin thread pool.

从输出中,我们可以看到,两个测试同时启动,并且在两个不同的线程中。请注意,输出可以从一个运行到另一个运行的变化。这是在使用ForkJoin线程池时预期的。

There is also an option to run all tests within the FirstParallelUnitTest class in the same thread. In the current scope, using parallelism and the same thread option is not viable so let’s expand our scope and add one more test class in the next section.

还有一个选项是在同一个线程中运行FirstParallelUnitTest类中的所有测试。在目前的范围内,使用并行和同一线程选项是不可行的,所以让我们扩大范围,在下一节增加一个测试类。

4. Test Parallelization Within a Module

4.模块内的测试并行化

Before we introduce a new property, we’ll create SecondParallelUnitTest class that has two methods similar to FirstParallelUnitTest:

在引入新属性之前,我们将创建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());
    }
}

Before we run our tests in the same batch, we need to set property:

在我们在同一批次中运行我们的测试之前,我们需要设置属性。

junit.jupiter.execution.parallel.mode.classes.default = concurrent

When we run both tests classes, we get the following output:

当我们运行这两个测试类时,我们得到以下输出。

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

From the output, we can see that all four tests run in parallel in different threads.

从输出中,我们可以看到,所有四个测试都在不同的线程中并行运行。

Combining two properties we mentioned in this and previous section and their values (same_thread and concurrent), we get four different modes of execution:

结合我们在这一节和上一节提到的两个属性及其值(same_thread和concurrent),我们得到四种不同的执行模式。

  1. (same_thread, same_thread) – all tests run sequentially
  2. (same_thread, concurrent) – tests from one class run sequentially, but multiple classes run in parallel
  3. (concurrent, same_thread) – tests from one class run parallel, but each class run separately
  4. (concurrent, concurrent) – tests run in parallel

5. Synchronization

5.同步化

In ideal situations, all our unit tests are independent and isolated. However, sometimes that’s hard to implement because they depend on shared resources. Then, when running tests in parallel, we need to synchronize over common resources in our tests. JUnit5 provides us with such mechanisms in the form of @ResourceLock annotation.

在理想情况下,我们所有的单元测试都是独立和隔离的。然而,有时这很难实现,因为它们依赖于共享资源。那么,当并行运行测试时,我们需要在测试中对公共资源进行同步。JUnit5以@ResourceLock注解的形式为我们提供了这种机制。

Similarly, as before, let’s create ParallelResourceLockUnitTest class:

同样,像以前一样,让我们创建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 allows us to specify which resource is shared and the type of lock we want to use (default is ResourceAccessMode.READ_WRITE). With the current setup, the JUnit engine will detect that our tests both use a shared resource and will execute them sequentially:

@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. Conclusion

6.结语

In this article, first, we covered how to configure parallel execution. Next, what are available strategies for parallelism and how to configure a number of threads? After that, we covered how different configurations affect test execution. In the end, we covered the synchronization of shared resources.

在这篇文章中,首先,我们介绍了如何配置并行执行。接下来,有哪些可用的并行策略,如何配置一些线程?之后,我们介绍了不同的配置如何影响测试执行。最后,我们介绍了共享资源的同步问题。

As always, code from this article can be found over on GitHub.

一如既往,本文的代码可以在GitHub上找到over