关于JUnit5 你必须知道的(三) 深入理解JUnit 5扩展机制

阅读这篇博客之前,我假设你对于JUnit 5有基本的认识,并且对其扩展机制有初步了解。如果你不清楚JUnit 基本的架构和使用,可以参考之前的三篇博客

关于JUnit5 你必须知道的(一) JUnit5架构和环境搭建
关于JUnit5 你必须知道的(二)JUnit 5的新特性
单元测试之JUnit 5 参数化测试使用手册

Extension Context,Namespace,Store原理

JUnit 5的扩展机制中有几个比较重要的概念: Extension Context, Store, Namespace

Extension Context

它是整个扩展机制的核心,每个扩展点的扩展方法都需要一个Extension Context 实例参数,用于获取执行的测试用例的信息和与Junit 引擎进行交互。

该接口定义了一系列方法,让我们来看下接口提供的方法:

    /**
	 * Get the parent extension context, if available.
	 */
	Optional<ExtensionContext> getParent();
	
	ExtensionContext getRoot();

在测试执行的时候Jupiter engine会创建一个测试节点树,我们的IDE往往也会用这么一个节点树来表示测试用例执行结果,如下图所示:
在这里插入图片描述
每一个容器都是一个带有子节点的内部节点,比方说一个测试类或者一个参数化测试方法;每一个独立的测试(eg.某个测试方法或者参数化测试的一种参数情形)都是叶子节点。

每个节点关联一个context, 如上描述在这个节点树中的节点都有其父节点,让其extension context指向其父节点的context。以此类推,root context就是与root node 关联的context。

下列方法则定义了test的唯一id, 展示名称, tag标签 :

 String getUniqueId();

 String getDisplayName();

 Set<String> getTags();

除此之外,context包含了获取对应的类信息和方法信息的一系列接口,借此扩展方法可以和测试类测试方法进行交互,比如说获取测试类的属性字段或者测试方法上的注解。

Optional<AnnotatedElement> getElement();
Optional<Method> getTestMethod();
Optional<Class<?>> getTestClass();
Optional<Object> getTestInstance();
Optional<Lifecycle> getTestInstanceLifecycle();

接下来还有junit的配置参数以及store

    ExtensionContext.Store getStore(ExtensionContext.Namespace var1);

Store

Extensions have to be stateless

理由如下:

  1. 我们并不确定一个extensions需要在何时怎样被初始化 (针对每个测试方法? 测试类? 还是每次执行? )
  2. Jupiter 并不想要去跟踪extension实例
  3. extensions之间可能需要交互,这就需要一个可靠的数据交换机制

所以extensions 必须是无状态的,并且在任何状态下都可以写入store或者从store获取对应的数据信息。

A store is a namespaced, hierarchical, key-value data structure.

让我们来看下Store的源码,它定义了如下接口:

public interface Store {
    
    
        Object get(Object var1);
        <V> V get(Object var1, Class<V> var2);
        default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
    
    
            V value = this.get(key, requiredType);
            return value != null ? value : defaultValue;
        }
        <K, V> Object getOrComputeIfAbsent(K var1, Function<K, V> var2);
        <K, V> V getOrComputeIfAbsent(K var1, Function<K, V> var2, Class<V> var3);
        void put(Object var1, Object var2);
        Object remove(Object var1);
        <V> V remove(Object var1, Class<V> var2);
}

有没有觉得很熟悉? 这不就跟Map的接口设计十分接近吗。让我们来验证下。

Store接口的实现是NamespaceAwareStore,其有两个属性对象ExtensionValuesStore,Namespace

    @Override
 	public Object get(Object key) {
    
    
		Preconditions.notNull(key, "key must not be null");
		return this.valuesStore.get(this.namespace, key);
	}

ExtensionValuesStore.class

   	private final ExtensionValuesStore parentStore;

    private final ConcurrentMap<CompositeKey, Supplier<Object>> storedValues = new ConcurrentHashMap<>(4);

	Object get(Namespace namespace, Object key) {
    
    
		Supplier<Object> storedValue = getStoredValue(new CompositeKey(namespace, key));
		return (storedValue != null ? storedValue.get() : null);
	}
	private Supplier<Object> getStoredValue(CompositeKey compositeKey) {
    
    
		Supplier<Object> storedValue = storedValues.get(compositeKey);
		if (storedValue != null) {
    
    
			return storedValue;
		}
		else if (parentStore != null) {
    
    
			return parentStore.getStoredValue(compositeKey);
		}
		else {
    
    
			return null;
		}
	}

很明显,Store的底层就是一个线程安全的HashMap, 其key 为参数namespace和key构建的一个组合key-compositeKey,也就保证了不同namespace下操作的数据相互隔离
重写了CompositeKey的equals方法,如果namespace和key相等,则认为两个CompositeKey相等

        @Override
		public boolean equals(Object o) {
    
    
			if (this == o) {
    
    
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
    
    
				return false;
			}
			CompositeKey that = (CompositeKey) o;
			return this.namespace.equals(that.namespace) && this.key.equals(that.key);
		}

Namespace

Namespace就非常简单了,源码如下:

  public static final ExtensionContext.Namespace GLOBAL = create(new Object());
        private final List<?> parts;

        public static ExtensionContext.Namespace create(Object... parts) {
    
    
            Preconditions.notEmpty(parts, "parts array must not be null or empty");
            Preconditions.containsNoNullElements(parts, "individual parts must not be null");
            return new ExtensionContext.Namespace(parts);
        }

        private Namespace(Object... parts) {
    
    
            this.parts = new ArrayList(Arrays.asList(parts));
        }

通过Object集合构造一个Namespace,只要集合相等则构造出来的Namespace就是相等的

我们可以通过不同的Namespace来隔离不同的extensions ,这样不同的extensions哪怕在操作同一个node的时候也不会发生冲突。当然如果有需要我们还可以利用这一特点,让不同的extensions共享同一份数据,这就看各自的业务需求了。

JUnit 5 扩展机制的应用

参数化测试的底层原理

JUnit 5为了方便我们进行参数化测试,定义了一系列的注解, 如下: @ValueSource,@NullSource,@EnumSource,@CsvSource,@CsvFileSource,@MethodSource,@ArgumentsSource

这些注解的底层都是通过@ArgumentsSource注解的方式实现的,我们可以看下@MethodSource的代码

@Target({
    
    ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(
    status = Status.EXPERIMENTAL,
    since = "5.0"
)
@ArgumentsSource(MethodArgumentsProvider.class)
public @interface MethodSource {
    
    
    String[] value() default {
    
    ""};
}

class MethodArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<MethodSource> {
    
    
    private String[] methodNames;
    //获取注解里定义的方法名称
    public void accept(MethodSource annotation) {
    
    
        this.methodNames = annotation.value();
    }

    public Stream<Arguments> provideArguments(ExtensionContext context) {
    
    
        Object testInstance = context.getTestInstance().orElse((Object)null);
        return Arrays.stream(this.methodNames).map((factoryMethodName) -> {
    
    
            return this.getMethod(context, factoryMethodName);
        }).map((method) -> {
    
    
            return ReflectionUtils.invokeMethod(method, testInstance, new Object[0]);
        }).flatMap(CollectionUtils::toStream).map(MethodArgumentsProvider::toArguments);
    }

    private Method getMethod(ExtensionContext context, String factoryMethodName) {
    
    
        if (StringUtils.isNotBlank(factoryMethodName)) {
    
    
            //如果名称中有#则在全部测试类里检索对应的方法名;否则就在当前测试类检索
            return factoryMethodName.contains("#") ? this.getMethodByFullyQualifiedName(factoryMethodName) : this.getMethod(context.getRequiredTestClass(), factoryMethodName);
        } else {
    
    
            //如果没有定义方法名称,则从当前测试类查找当前测试方法同名的参数方法
            return this.getMethod(context.getRequiredTestClass(), context.getRequiredTestMethod().getName());
        }
    }
}

那么ArgumentsProvider又是怎么对测试方法进行扩展的呢? 这就需要看下Junit 5参数化测试的实现方式了。Junit 5的参数化测试是通过测试模版实现的 , 不了解的可以参考下如下文章: JUnit 5模板测试 ,参数化测试的核心类在org.junit.jupiter.junit-jupiter-params 里的ParameterizedTestExtension

class ParameterizedTestExtension implements TestTemplateInvocationContextProvider {
    
    

	private static final String METHOD_CONTEXT_KEY = "context";

    //校验是否支持该测试模板: 1,有测试方法 2.测试方法上有ParameterizedTest注解
	@Override
	public boolean supportsTestTemplate(ExtensionContext context) {
    
    
		if (!context.getTestMethod().isPresent()) {
    
    
			return false;
		}

		Method testMethod = context.getTestMethod().get();
		if (!isAnnotated(testMethod, ParameterizedTest.class)) {
    
    
			return false;
		}

		ParameterizedTestMethodContext methodContext = new ParameterizedTestMethodContext(testMethod);
	    //..
	    //将ParameterizedTestMethodContext存入对应的Store
		getStore(context).put(METHOD_CONTEXT_KEY, methodContext);

		return true;
	}

	@Override
	public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
			ExtensionContext extensionContext) {
    
    

		Method templateMethod = extensionContext.getRequiredTestMethod();
		String displayName = extensionContext.getDisplayName();
		//获取对应的Store(以ParameterizedTestExtension和RequiredTestMethod为namespace),然后从中获取ParameterizedTestMethodContext对象也就是supportsTestTemplate方法中存入Store的参数化测试方法的上下文
		ParameterizedTestMethodContext methodContext = getStore(extensionContext)//
				.get(METHOD_CONTEXT_KEY, ParameterizedTestMethodContext.class);
		ParameterizedTestNameFormatter formatter = createNameFormatter(templateMethod, methodContext, displayName);
		AtomicLong invocationCount = new AtomicLong(0);

		// @formatter:off
		//先找到测试方法上的ArgumentsSource注解,然后获取其value值(也就是用于获取参数的类)
		return findRepeatableAnnotations(templateMethod, ArgumentsSource.class)
				.stream()
				.map(ArgumentsSource::value)
				//通过反射初始化ArgumentsProvider实例
				.map(this::instantiateArgumentsProvider)
				//如果ArgumentsProvider实例是AnnotationConsumer类型则进行对应的初始化操作
				.map(provider -> AnnotationConsumerInitializer.initialize(templateMethod, provider))      //执行ArgumentsProvider对象实现的provideArguments方法,也就是我们实现ArgumentsProvider时需要实现的方法
				.flatMap(provider -> arguments(provider, extensionContext))
				.map(Arguments::get)
				.map(arguments -> consumedArguments(arguments, methodContext))
				 //获取到对应的参数信息之后创建InvocationContext(目前只是获得了执行参数的InvocationContext,还没有真正执行参数化测试的方法)
				.map(arguments -> createInvocationContext(formatter, methodContext, arguments))
				.peek(invocationContext -> invocationCount.incrementAndGet())
				.onClose(() ->
						Preconditions.condition(invocationCount.get() > 0,
								"Configuration error: You must configure at least one set of arguments for this @ParameterizedTest"));
		// @formatter:on
	}

通过上述ParameterizedTestInvocationContext和arguments创建了ParameterizedTestParameterResolver实例,源码如下:

class ParameterizedTestInvocationContext implements TestTemplateInvocationContext {
    
    

	private final ParameterizedTestNameFormatter formatter;
	private final ParameterizedTestMethodContext methodContext;
	private final Object[] arguments;

	ParameterizedTestInvocationContext(ParameterizedTestNameFormatter formatter,
			ParameterizedTestMethodContext methodContext, Object[] arguments) {
    
    
		this.formatter = formatter;
		this.methodContext = methodContext;
		this.arguments = arguments;
	}

	@Override
	public String getDisplayName(int invocationIndex) {
    
    
		return this.formatter.format(invocationIndex, this.arguments);
	}

	@Override
	public List<Extension> getAdditionalExtensions() {
    
    
		return singletonList(new ParameterizedTestParameterResolver(this.methodContext, this.arguments));
	}

}

其提供的resolveParameter方法可以帮我们将用户输入的参数和测试方法需要的参数做匹配转换。那么最重要的测试方法的执行是在哪里呢,debug了一下,发现其入口是在
TestMethodTestDescriptor的execute方法。在该方法里我们可以看到很多扩展方法的调用,以invokeBeforeEachCallbacks为例,我们可以看下其简单实现

@Override
	public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context,
			DynamicTestExecutor dynamicTestExecutor) throws Exception {
    
    
		ThrowableCollector throwableCollector = context.getThrowableCollector();

		// @formatter:off
		invokeBeforeEachCallbacks(context);
			if (throwableCollector.isEmpty()) {
    
    
				invokeBeforeEachMethods(context);
				if (throwableCollector.isEmpty()) {
    
    
					invokeBeforeTestExecutionCallbacks(context);
					if (throwableCollector.isEmpty()) {
    
    
					    //这里就是真正执行参数化测试方法的地方
						invokeTestMethod(context, dynamicTestExecutor);
					}
					invokeAfterTestExecutionCallbacks(context);
				}
				invokeAfterEachMethods(context);
			}
		invokeAfterEachCallbacks(context);
		if (isPerMethodLifecycle(context)) {
    
    
			invokeTestInstancePreDestroyCallbacks(context);
		}
		// @formatter:on

		throwableCollector.assertEmpty();

		return context;
	}
	
	private void invokeBeforeEachCallbacks(JupiterEngineExecutionContext context) {
    
    
		invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeEachCallback.class, context,
			(callback, extensionContext) -> callback.beforeEach(extensionContext));
	}
	
    private <T extends Extension> void invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(Class<T> type,
			JupiterEngineExecutionContext context, CallbackInvoker<T> callbackInvoker) {
    
    
        //获取注册的Extension
		ExtensionRegistry registry = context.getExtensionRegistry();
		ExtensionContext extensionContext = context.getExtensionContext();
		ThrowableCollector throwableCollector = context.getThrowableCollector();
        //根据不同类型的扩展接口执行不同的方法(这里执行的是beforeEach接口方法)
		for (T callback : registry.getExtensions(type)) {
    
    
			throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext));
			if (throwableCollector.isNotEmpty()) {
    
    
				break;
			}
		}
	}

再来看下真正执行测试方法invokeTestMethod(context, dynamicTestExecutor)的源码

protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) {
    
    
		ExtensionContext extensionContext = context.getExtensionContext();
		ThrowableCollector throwableCollector = context.getThrowableCollector();

		throwableCollector.execute(() -> {
    
    
			try {
    
    
				Method testMethod = getTestMethod();
				Object instance = extensionContext.getRequiredTestInstance();
				//调用的是ExecutableInvoker的invoke方法
				executableInvoker.invoke(testMethod, instance, extensionContext, context.getExtensionRegistry(),
					interceptorCall);
			}
			catch (Throwable throwable) {
    
    
				BlacklistedExceptions.rethrowIfBlacklisted(throwable);
				invokeTestExecutionExceptionHandlers(context.getExtensionRegistry(), extensionContext, throwable);
			}
		});
	}
---------------------------------ExecutableInvoker---------------------------------
	public <T> T invoke(Method method, Object target, ExtensionContext extensionContext,
			ExtensionRegistry extensionRegistry, ReflectiveInterceptorCall<Method, T> interceptorCall) {
    
    

		@SuppressWarnings("unchecked")
		Optional<Object> optionalTarget = (target instanceof Optional ? (Optional<Object>) target
				: Optional.ofNullable(target));
		//获取对应的参数信息(底层调用了ParameterResolver的resolveParameter方法)		  
		Object[] arguments = resolveParameters(method, optionalTarget, extensionContext, extensionRegistry);
		MethodInvocation<T> invocation = new MethodInvocation<>(method, optionalTarget, arguments);
		//底层就是使用了反射机制,就不再进一步跟踪了
		return invoke(invocation, invocation, extensionContext, extensionRegistry, interceptorCall);
	}

关于JUnit5的内容到这里就结束了,感兴趣的可以去官网或者下面的网站自行查阅资料。

参考资料:
JUnit 5 Extension Model: How To Create Your Own Extensions

猜你喜欢

转载自blog.csdn.net/qq_35448165/article/details/108684339