并行测试

重要性:★★☆☆☆

缺省情况下,JUnit Pupiter在单个线程里面顺序执行各个测试方法。要改成并发执行,首先需要将JUnit平台配置参数junit.jupiter.execution.parallel.enabled设置为true。但这只是个必要条件,而不是个充分条件。

在将JUnit平台配置参数junit.jupiter.execution.parallel.enabled设置为true之后,在测试树中的测试节点是否会真正并发执行取决于测试的执行模式。执行模式有两个:

  • SAME_THREAD:缺省模式。强制测试节点在和父节点相同的线程内执行。例如,当在一个测试方法上设定执行模式为SAME_THREAD时,测试方法将在和位于同一个测试类中的@BeforeAll@AfterAll生命周期方法的相同的线程中执行。
  • CONCURRENT:并发执行测试,除非被一个资源锁强制在同一个线程中执行。

所以,要将整个系统的测试设置为并行,可以在JUnit平台设置文件junit-platform.properties文件中同时设置以下两个参数:

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

如果你想将某个测试节点(测试类、嵌套测试类、测试方法等)设置为其他执行模式,可以使用@Execution注解:

package yang.yu.tdd.concurrent;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

@Execution(ExecutionMode.CONCURRENT)
public class ConcurrentTest {

    @Test
    void test1() {
    }

    @Test
    @Execution(ExecutionMode.SAME_THREAD)
    void test2() {
    }
}

1. 配置

可以通过使用ParallelExecutionConfigurationStrategy实现类配置并发度和最大线程池尺寸等并行属性。JUnit平台提供了两个现成的策略:dynamicfixed,还可以实现自己的一个custom策略。

  • dynamic:基于CPU处理器/核心的数量乘以junit.jupiter.execution.parallel.config.dynamic.factor配置参数的值动态计算并发度。
  • fixed:使用junit.jupiter.execution.parallel.config.fixed.parallelism配置参数设定并发度。
  • custom:通过设置junit.jupiter.execution.parallel.config.custom.class配置参数指定一个ParallelExecutionConfigurationStrategy接口的实现类来设定并发配置。

如果没有设定并发策略,系统默认采用dynamic策略,并将junit.jupiter.execution.parallel.config.dynamic. factor配置参数设置为1。这意味着并发度等于电脑的CPU/核心的数量。

2. 同步

除了通过@Execution注解设置测试并发执行模式之外,JUnit Jupiter还提供了另外一个基于注解的声明式同步机制。@ResourceLock注解允许你声明一个测试类或测试方法使用一个共享的、需要同步访问的资源来保证测试的可靠执行。这个共享资源通过一个唯一的名称来标识。这个名称可以是自定义的,也可以是系统预定义的Resources常量之一:SYSTEM_PROPERTIES, SYSTEM_OUT, SYSTEM_ERR, LOCALETIME_ZONE

下面示例中的测试类如果没有使用@ResourceLock注解而在并发模式下执行,测试是脆弱的,测试有时会通过,有时会因为对同一个JVM系统属性先写后读的内在竞态条件而失败。当使用@ResourceLock注解声明共享资源之后,JUnit Jupiter使用这个信息保证相互冲突的测试不会并行执行。

@ResourceLock注解中还可以指定资源的访问模式。两个需要READ访问同一个共享资源的测试可以并发执行,但任何一个测试需要对共享资源进行READ_WRITE时,两个测试只能顺序执行。

package yang.yu.tdd.concurrent;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ResourceLock;

import java.util.Properties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ;
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES;

@Execution(CONCURRENT)
class SharedResourcesDemo {

    private Properties backup;

    @BeforeEach
    void backup() {
        backup = new Properties();
        backup.putAll(System.getProperties());
    }

    @AfterEach
    void restore() {
        System.setProperties(backup);
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
    void customPropertyIsNotSetByDefault() {
        assertThat(System.getProperty("my.prop")).isNull();
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
    void canSetCustomPropertyToApple() {
        System.setProperty("my.prop", "apple");
        assertThat(System.getProperty("my.prop")).isEqualTo("apple");
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
    void canSetCustomPropertyToBanana() {
        System.setProperty("my.prop", "banana");
        assertThat(System.getProperty("my.prop")).isEqualTo("banana");
    }
}

results matching ""

    No results matching ""