多线程执行测试用例
我们的测试用例默认是一种顺序执行的,可以自己规定按照名称或者设置order来顺序执行,但是有时候我们为了提高测试效率,节省测试时间,需要并行去执行测试用例
本文目录
一、实现并发执行
在 junit-platform.properties文件 该文件名是junit5默认的不可更改
,中增加并发规则配置
1.并发规则
需要在配置文件中增加下面一行配置,用来开启并发测试
junit.jupiter.execution.parallel.enabled = true
下面测试用例,共编写3个测试类,每个测试方法打印出当前的线程名称以及打印所在的测试类:
//测试类1
public class Concurrent1_Test {
@Test
void test1(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test");
}
@Test
void test2(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test");
}
@Test
void test3(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test");
}
}
//测试类2
public class Concurrent2_Test {
@Test
void test1(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent2_Test");
}
@Test
void test2(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent2_Test");
}
@Test
void test3(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent2_Test");
}
}
//测试类3
public class Concurrent3_Test {
@Test
void test1(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent3_Test");
}
@Test
void test2(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent3_Test");
}
@Test
void test3(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent3_Test");
}
}
在开启并行测试配置前执行:得到结果如下:
三个测试类的方法法都在main进程里
开启配置后,执行用例,会发现 进程是ForkJoinPool-1-worker-3,说明 junit5开始使用了ForkJoin线程池,但是看结果的话,测试用例并没有并行执行
因此,该配置,是用来声明可以用来并行执行测试用例了,但是并没有声明并行测试的规则
下面有几种执行规则的配置:
same_thread是默认值,默认同一线程下执行
concurrent 是并发执行
测试方法并发执行
增加配置如下,是配置所有测试方法并发执行
#所有的测试方法并行执行
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
这种配置方式,不知道为什么我这里还是按照类去并行执行的 方法并没有并行执行,有待解决
已找到原因,原因是配置文件中有junit.jupiter.testmethod.order.default
该配置用来配置用例执行顺序的,与并发配置出现冲突,删除后好了:
执行结果如下图所示,9个测试方法有9个线程执行
测试类并发,测试方法不并发
#所有的测试类 并行执行
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
执行结果输出如下,由此看出,执行的顺序全部打乱了,并且不同的测试类使用的不同的线程去执行的,同一个测试类在同一线程下执行
测试类串行,测试方法并行
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
测试类测试方法都并行
该配置的执行效果与只开启测试方法并发执行效果一致
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
2.配置并发策略
上面的配置只是声明了并发规则,并没有实现并发数的这样一个实现,在测试过程中还需要一个多并发,需要规定某个测试节点的并发数
使用配置添加并发执行策略,并发策略共有三个值:dynamic、fixed、custom ,默认值 dynamic,其中custom需要通过自定义形式来配置并发线程数
dynamic(动态)
动态策略(并发数是动态的)
并发数是根据 处理器/内核数 ✖ 因子参数(默认1)
来确定线程数
#开启策略为dynamic
junit.jupiter.execution.parallel.config.strategy = dynamic
设置dynamic的并发数
dynamic 的系数配置项:默认是1
junit.jupiter.execution.parallel.config.dynamic.factor
fixed(固定)
固定策略(并发数是写死的)
所需要的并发数依赖于预定义的并发数
#fixed固定策略
junit.jupiter.execution.parallel.config.strategy = fixed
如下所示,配置并发数为2
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 2
执行测试用例,得到 只有两个线程执行:
custom(自定义)
在配置文件配置custom策略,并发数设置3
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.config.strategy = custom
junit.jupiter.execution.parallel.config.custom.class = com.xx.CustomStrategy//自定义策略类的路径
junit.jupiter.execution.parallel.config.custom.parallelism = 3
自定义策略方法,实际上是参考fixed和dynamic,fiexd的策略是实现了ParallelExecutionConfigurationStrategy
接口,因此自定义custom也需要实现该接口
package com.ceshiren;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.support.hierarchical.ParallelExecutionConfiguration;
import org.junit.platform.engine.support.hierarchical.ParallelExecutionConfigurationStrategy;
/**
* 使用自定义类型 custom实现并发策略
*/
public class CustomStrategy implements ParallelExecutionConfigurationStrategy {
@Override
public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) {
//如何自定义并发策略,参考ParallelExecutionConfigurationStrategy的其他实现类,例如fixed
// int parallelism = (Integer)configurationParameters.get("fixed.parallelism", Integer::valueOf).orElseThrow(() -> {
// return new JUnitException(String.format("Configuration parameter '%s' must be set", "fixed.parallelism"));
// });
// return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism, 30);
//双冒号是lambda表达式的用法,相当于 x -> Integer.valueOf(x)
int count = configurationParameters.get("custom.parallelism", Integer::valueOf).orElseThrow(() -> {
return new JUnitException(String.format("Configuration parameter '%s' must be set", "custom.parallelism"));
});
return new ParallelExecutionConfiguration() {
@Override
public int getParallelism() {
return count;
}
@Override
public int getMinimumRunnable() {
return count;
}
@Override
public int getMaxPoolSize() {
return count;
}
@Override
public int getCorePoolSize() {
return count;
}
@Override
public int getKeepAliveSeconds() {
return count;
}
};
}
}
不过在我看来自定义custom的方式和fixed的方式没有啥区别,还是使用fixed就行,如果fiexd不满足使用,那可能就使用custom自己开发功能
3.使用注解@Execution
也需要在配置文件开启并行测试开关
junit.jupiter.execution.parallel.enabled = true
需要在类上面增加注解@Execution(ExecutionMode.CONCURRENT)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
/**
* 并发测试从junit5.3以后才有
*/
@Execution(ExecutionMode.CONCURRENT)
public class Concurrent1_Test {
@Test
void test1(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test1");
}
@Test
void test2(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test2");
}
@Test
void test3(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test3");
}
@Test
void test4(){
System.out.println(Thread.currentThread().getName()+"=> Concurrent1_Test4");
}
}
直接执行这个类,方法是 并发执行的
将注解放在方法上,将类上的注解改成默认(串行),第一个和第二个方法增加注解,第三和第四不增加注解:
@Execution(ExecutionMode.SAME_THREAD)
public class Concurrent1_Test {
@Test
@Execution(ExecutionMode.CONCURRENT)
void test1() {
System.out.println(Thread.currentThread().getName() + "=> Concurrent1_Test1");
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void test2() {
System.out.println(Thread.currentThread().getName() + "=> Concurrent1_Test2");
}
@Test
void test3() {
System.out.println(Thread.currentThread().getName() + "=> Concurrent1_Test3");
}
@Test
void test4() {
System.out.println(Thread.currentThread().getName() + "=> Concurrent1_Test4");
}
}
执行结果如下,方法一和方法二是并发执行 方法三和方法四在另一个线程中串行执行,由此看出来 将注解放在方法上比放在类上的优先级更高
4.并发策略+@Execution
在配置文件中开启并发执行,并设置每个测试方法并发执行 配置并发数为 3,并使用注解
#custom 自定义
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = custom
junit.jupiter.execution.parallel.config.custom.class = com.ceshiren.CustomStrategy
junit.jupiter.execution.parallel.config.custom.parallelism = 20
前面我们没有配置并发数,只是用注解的话,会根据这个注解来开启线程数,如果我们配置了线程数,那么会限制创建的并线程数
下面执行 全部的3个测试类,其中包含带有注解的测试类
执行结果如下:我们一共允许开启20个线程,这个数超过了所需要的线程数,我们也看到了:
1.没有使用注解的类中的方法全部都并发执行,
2.使用注解的类中 标明要并发的方法并发执行(类1中方法12是并发执行),而没有声明并发执行的方法是串行执行(类1中方法34是串行执行)
由此看出来,配置文件是全局生效,注解针对于类和方法,其中注解的优先级大于配置(按照类进行并发、按照方法并发)
二、多线程的数据共享
有些时候资源是需要被共享的,多个线程同时访问同一资源,但是多并发使用同一资源容易造成问题,因此又出来了锁机制来实现并发安全,在并发操作中操作同一资源需要上锁
- @ResourceLock注解实现资源同步,该注解相当于synchronized、@sychronized
- @ResourceLock 为测试类和测试方法提供同步机制
- 该注解有2个参数:一个是String型参数,指定资源值,另一个是访问模式-ResourceAccessMode ,访问模式可以是READ(只读)、 READ_WRITE(读写)
预定义资源:
- Resources.SYSTEM_PROPERTIES:表示java系统属性
- Resources.SYSTEM_OUT:表示当前进程的标准输出流
- Resources.SYSTEM_ERR:表示当前进程的标准错误流
- Resources.LOCALE:当前JVM实例的默认语言环境
- Resources.TIMEZONE:当前JVM实例的默认时区
配置多线程执行测试用例,给测试类增加@Execution(ExecutionMode.CONCURRENT),方法使用@ResourceLock注解来读写同一个系统资源
@Execution(ExecutionMode.CONCURRENT)
public class ResourceLockTest {
Properties properties;
@BeforeEach
void setUpClass() {
properties = new Properties(System.getProperties());
}
@Test
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
void test1() {
assertNull(System.getProperty("custom"));
}
@Test
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)
void test2() {
System.setProperty("custom", "custom_value");
assertEquals("custom_value", System.getProperty("custom"));
}
@Test
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)
void test3() {
System.setProperty("custom", "custom_value2");
assertEquals("custom_value2", System.getProperty("custom"));
}
}
自定义资源,这里用并发读写map为例,定义一个Map 给他增加增删改查方法,并发对其进行增删改查操作,验证结果是否正确
@Execution(ExecutionMode.CONCURRENT)
public class ResourceLock2Test {
public static final String GLOBAL_USER = "com.xxx.entity.User";//自定义资源类
@BeforeEach
void before() {
User.clear();
}
@Test
@ResourceLock(value = GLOBAL_USER,mode = ResourceAccessMode.READ)
void get() {
System.out.println("get => " + User.getUser());
assertTrue(User.getUser().isEmpty());
}
@Test
@ResourceLock(value = GLOBAL_USER,mode = ResourceAccessMode.READ_WRITE)
void add() {
User.add(1, "小明");
System.out.println("add => " + User.getUser());
assertEquals("小明", User.get(1));
}
@Test
@ResourceLock(value = GLOBAL_USER,mode = ResourceAccessMode.READ_WRITE)
void update() {
User.update(1, "小红");
System.out.println("update => " + User.getUser());
assertEquals("小红", User.get(1));
}
@Test
@ResourceLock(value = GLOBAL_USER,mode = ResourceAccessMode.READ_WRITE)
void del() {
User.add(2, "思思");
System.out.println("remove => " + User.getUser());
User.del(2);
assertNull(User.get(2));
}
}
通过向 maven surefire 插件提供参数
使用maven surefire 可以使用mvn命令:mvn test -Dtest 来执行测试用例
在这里可以mvn命令来进行并行测试:
mvn test -Djunit.jupiter.execution.parallel.enabled=true
待补充相关知识点
通过向 JVM 提供系统属性
待补充相关知识点
注意:使用配置来进行并发执行用例时,配置文件中不能同时有
junit.jupiter.testmethod.order.default
这个配置,该配置时用来自定义用例执行顺序的,会与并发执行 存在冲突,导致并发执行的配置不生效