当前位置: 首页 > 工具软件 > JUnit Max > 使用案例 >

Junit5的顺序与并发执行

卢志强
2023-12-01

首先参考博客学习并行执行配置参数: Junit5的并行执行

有些场景下,单个测试类必须保证顺序性,在Junit5当中提供了两种方式:第一种是根据测试方法的字母先后顺序,另外一种是根据在测试上面添加注解来实现的。

对于字母的方式,比较简单,直接在测试类上面的添加如下的注解

@TestMethodOrder(MethodOrderer.Alphanumeric.class)

对于通过注解的方式,需要测试类和测试方法同时添加注解TestMethodOrderOrder,前者指定使用的算法,后者指定执行的顺序(按照数字的小到大顺序执行),如下所示

package com.xquant.platform.test.asserts.client.acc;

import com.xquant.platform.test.asserts.client.AbstractExcelContextTests;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 * Created in 2020/9/7 14:57 by guanglai.zhou
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CashAccountExtTest extends AbstractExcelContextTests {

    /**
     * 创建外部资金账户
     */
    @Order(1)
    @Test
    public void testA_addCashAccExt() {
        String resourceLocation = "classpath*:excel/acc/CashAccountExtTest.xlsx";
        testSheetInExcel(resourceLocation, "addCashAccExt");
    }

    /**
     * 修改外部资金账户
     */
    @Order(2)
    @Test
    public void testB_updateCashAccExt() {
        String resourceLocation = "classpath*:excel/acc/CashAccountExtTest.xlsx";
        testSheetInExcel(resourceLocation, "updateCashAccExt");
    }
    /**
     * 删除外部资金账户 依赖测试 test1AddCashAccExt 因此必须在test1AddCashAccExt之后
     */
    @Order(3)
    @Test
    public void testC_deleteAccountExt() {
        String resourceLocation = "classpath*:excel/acc/CashAccountExtTest.xlsx";
        testSheetInExcel(resourceLocation, "deleteAccountExt");
    }
}

当然用户可以自定义属于自己的算法。实现Junit5的接口org.junit.jupiter.api.MethodOrderer并作为TestMethodOrder注解的属性即可。由于以上两种方式能满足绝大部分要求,此处不再详述。

junit4中的@FixMethodOrder注解在junit5中测试方法的执行顺序没有任何作用

对应的源码如下所示:
org.junit.jupiter.engine.discovery.MethodOrderingVisitor

@Override
public void visit(TestDescriptor testDescriptor) {
	if (testDescriptor instanceof ClassBasedTestDescriptor) {
		ClassBasedTestDescriptor classBasedTestDescriptor = (ClassBasedTestDescriptor) testDescriptor;
		try {
			orderContainedMethods(classBasedTestDescriptor, classBasedTestDescriptor.getTestClass());
		}
		catch (Throwable t) {
			BlacklistedExceptions.rethrowIfBlacklisted(t);
			logger.error(t, () -> "Failed to order methods for " + classBasedTestDescriptor.getTestClass());
		}
	}
}
private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescriptor, Class<?> testClass) {
    // 查找测试方法上面的注解 包括元注解 接口 父类中的注解 TestMethodOrder
	findAnnotation(testClass, TestMethodOrder.class)
		     // 获取注解中的值 也就是MethodOrderer实现类 		
			.map(TestMethodOrder::value)
			// 通过反射创建实例 这个实现类必须包含默认的构造器
			.map(ReflectionUtils::newInstance)
			// 如果存在构造的对象
			.ifPresent(methodOrderer -> {
                // 根据测试类描述获取测试方法描述列表
				Set<? extends TestDescriptor> children = classBasedTestDescriptor.getChildren();
                // 获取没测试方法 
				List<TestDescriptor> nonMethodTestDescriptors = children.stream()
						// 进行过滤 非测试方法返回true
						.filter(testDescriptor -> !(testDescriptor instanceof MethodBasedTestDescriptor))
						 // 进行收集
						.collect(Collectors.toList());
				
				List<DefaultMethodDescriptor> methodDescriptors = children.stream()
						// 过滤 类型判断
						.filter(MethodBasedTestDescriptor.class::isInstance)
						// 映射 类型强转
						.map(MethodBasedTestDescriptor.class::cast)
						// 映射 实例化类对象
						.map(DefaultMethodDescriptor::new)
						// 收集对象
						.collect(toCollection(ArrayList::new));

				// Make a local copy for later validation
				Set<DefaultMethodDescriptor> originalMethodDescriptors = new LinkedHashSet<>(methodDescriptors);
                // 进行排序 
				methodOrderer.orderMethods(
					new DefaultMethodOrdererContext(methodDescriptors, testClass, this.configuration));
				
				// 比较排序前后的方法数量
				int difference = methodDescriptors.size() - originalMethodDescriptors.size();

				if (difference > 0) {
					logger.warn(() -> String.format(
						"MethodOrderer [%s] added %s MethodDescriptor(s) for test class [%s] which will be ignored.",
						methodOrderer.getClass().getName(), difference, testClass.getName()));
				}
				else if (difference < 0) {
					logger.warn(() -> String.format(
						"MethodOrderer [%s] removed %s MethodDescriptor(s) for test class [%s] which will be retained with arbitrary ordering.",
						methodOrderer.getClass().getName(), -difference, testClass.getName()));
				}

				Set<TestDescriptor> sortedMethodTestDescriptors = methodDescriptors
						// 获取流对象
						.stream()
						// 进行过滤 排序之前包含
						.filter(originalMethodDescriptors::contains)
						// 获取测试描述
						.map(DefaultMethodDescriptor::getTestDescriptor)
						// 收集到LinkedHashSet 因为需要排序
						.collect(toCollection(LinkedHashSet::new));

				// Currently no way to removeAll or addAll children at once.
				// 通过以下操作保证了classBasedTestDescriptor中测试方法的顺序性
				Stream.concat(sortedMethodTestDescriptors.stream(), nonMethodTestDescriptors.stream())//
						.forEach(classBasedTestDescriptor::removeChild);
				Stream.concat(sortedMethodTestDescriptors.stream(), nonMethodTestDescriptors.stream())//
						.forEach(classBasedTestDescriptor::addChild);

				// Note: MethodOrderer#getDefaultExecutionMode() is guaranteed
				// to be invoked after MethodOrderer#orderMethods().
				methodOrderer.getDefaultExecutionMode()
						// 映射执行模式
						.map(JupiterTestDescriptor::toExecutionMode)
						// 设置默认方法执行模式
						.ifPresent(classBasedTestDescriptor::setDefaultChildExecutionMode);
			});
}

org.junit.jupiter.engine.descriptor.JupiterTestDescriptor#toExecutionMode

public static ExecutionMode toExecutionMode(org.junit.jupiter.api.parallel.ExecutionMode mode) {
	switch (mode) {
		case CONCURRENT:
			return ExecutionMode.CONCURRENT;
		case SAME_THREAD:
			return ExecutionMode.SAME_THREAD;
	}
	throw new JUnitException("Unknown ExecutionMode: " + mode);
}

测试方法 protected final Set children = Collections.synchronizedSet(new LinkedHashSet<>(16));

对应的排序实现org.junit.jupiter.api.MethodOrderer.OrderAnnotation

  1. 根据@Order注解进行排序
  2. 相同order值的方法任意顺序
  3. 没有包含注解的会赋默认值 Integer.MAX_VALUE / 2 不是0
  4. 排序结果为按照order值从小到大排序 因为一般情况下默认值会排到最后
/**
 * {@code MethodOrderer} that sorts methods based on the {@link Order @Order}
 * annotation.   根据@Order注解进行排序
 *
 * <p>Any methods that are assigned the same order value will be sorted
 * arbitrarily adjacent to each other.    相同order值的方法任意顺序
 *
 * <p>Any methods not annotated with {@code @Order} will be assigned the
 * {@link org.junit.jupiter.api.Order#DEFAULT default order} value which will
 * effectively cause them to appear at the end of the sorted list, unless
 * certain methods are assigned an explicit order value greater than the default
 * order value. Any methods assigned an explicit order value greater than the
 * default order value will appear after non-annotated methods in the sorted
 * list.  没有包含注解的会赋默认值  Integer.MAX_VALUE / 2  不是0
 */
class OrderAnnotation implements MethodOrderer {

	/**
	 * Sort the methods encapsulated in the supplied
	 * {@link MethodOrdererContext} based on the {@link Order @Order}
	 * annotation.
	 */
	@Override
	public void orderMethods(MethodOrdererContext context) {
		context.getMethodDescriptors()
		// import static java.util.Comparator.comparingInt;
		// java.util.Comparator#comparingInt(ToIntFunction<? super T> keyExtractor) 比较整数值
			   .sort(comparingInt(OrderAnnotation::getOrder));
	}

	private static int getOrder(MethodDescriptor descriptor) {
		return descriptor.findAnnotation(Order.class)
						 // 获取Order值
						 .map(Order::value)
						 // 不包含注解则取默认值
						 .orElse(Order.DEFAULT);
	}
}

通过以上的方式,我们定义了一个测试类内部的方法执行顺序。

在实际测试时,为了提高测试的速度,我们还需要设置测试并发执行。现在方法内已经设置了顺序执行,还能并发执行吗?可以的。

因为测试内部是顺序执行(同一个线程),而每个测试之间是可以并发执行的,没有顺序的要求,在文件junit-platform.properties按照如下配置即可:

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
 类似资料: