并行测试
重要性:★★☆☆☆
缺省情况下,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平台提供了两个现成的策略:dynamic
和fixed
,还可以实现自己的一个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
, LOCALE
或 TIME_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");
}
}