鸽了好几个月哈哈,原文地址:https://www.dnocm.com/articles/cherry/junit-5-info/
与 Junit4 不同,Junit5 提供了一个统一的一个扩展API。不过在之前,先看下另一个 Junit5 的重要特性--组合注解
组合注解
在官方文档中,这部分与注解部分一同讲的,但我将它移到此处,因为绝大多数情况下,他都是与扩展API一同使用。
组合注解,顾名思义,当一个注解上存在其他的Junit注解时,同时也继承这些注解的语义
例如:组合Tag与Test注解
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface Fast {
}
@Fast
void asserts() {
assertTrue(true);
}
Extend API
在 Junit5 中通过 @ExtendWith
注解实现添加扩展。
@ExtendWith(DatabaseExtension.class)
public class SimpleTest {
// code
}
@Slf4j
public class DatabaseExtension implements BeforeAllCallback, AfterAllCallback {
@Override
public void beforeAll(ExtensionContext extensionContext) throws Exception {
log.info("连接数据库");
}
@Override
public void afterAll(ExtensionContext extensionContext) throws Exception {
log.info("关闭数据库");
}
}
@ExtendWith
提供了扩展的入口,具体的实现通过实现对应的接口,例如上面的 DatabaseExtension
实现 BeforeAllCallback
,AfterAllCallback
在Junit中,存在许多扩展接口
ExecutionCondition
定义执行条件,满足条件时才能执行,下面是一个例子
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(PassConditionalExtension.class)
@Test
public @interface Pass {
String value();
}
public class PassConditionalExtension implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
return AnnotationUtils.findAnnotation(context.getElement(), Pass.class)
.map(Pass::value)
.filter("我很帅"::equals)
.map(item -> ConditionEvaluationResult.enabled("pass"))
.orElse(ConditionEvaluationResult.disabled("pass is not okay!"));
}
}
public class ConditionalTest {
@Pass("密码不对不执行")
void notExec() {
// code...
}
@Pass("我很帅")
void exec() {
// code...
}
}
TestInstanceFactory
定义测试实例,只能用于class上,暂时想不到例子,跳过~~
TestInstancePostProcessor
对测试实例处理,通常用于注入依赖,暂时想不到例子,跳过~~
TestInstancePreDestroyCallback
当测试实例销毁前调用,暂时想不到例子,跳过~~
ParameterResolver
处理参数,见下面例子
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface BookInject {
String title();
int price() default 0;
}
public class BookParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.isAnnotated(BookInject.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.findAnnotation(BookInject.class)
.map(book -> Book.of(book.title(), book.price()))
.orElse(null);
}
}
@Slf4j
public class BookParameterTest {
@Test
@ExtendWith(BookParameterResolver.class)
void exec(@BookInject(title = "删库") Book book) {
log.info(book.toString());
}
}
TestWatcher
监听测试用例的执行结果
@Slf4j
public class LogTestWatcher implements TestWatcher {
@Override
public void testSuccessful(ExtensionContext context) {
log.info("wow, 成功了!");
}
@Override
public void testAborted(ExtensionContext context, Throwable cause) {
// 终止
}
@Override
public void testDisabled(ExtensionContext context, Optional reason) {
// 取消(跳过)
}
@Override
public void testFailed(ExtensionContext context, Throwable cause) {
// 失败
}
}
生命周期回调
在一开始的例子中就是生命周期的回调,这里不写例子拉,他们执行的先后顺序如下
- BeforeAllCallback
- BeforeEachCallback
- BeforeTestExecutionCallback
- AfterTestExecutionCallback
- AfterEachCallback
- BeforeEachCallback
- AfterAllCallback
TestExecutionExceptionHandler
处理异常,如果存在一些自定义的运行时异常,这是很有用的,可以做些处理
public class IgnoreExceptionExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
if (throwable instanceof Exception) {
return;
}
throw throwable;
}
}
public class SimpleTest {
@Test
@ExtendWith(IgnoreExceptionExtension.class)
void exec2() throws Exception {
throw new Exception("被忽略");
}
@Test
@ExtendWith(IgnoreExceptionExtension.class)
void exec3() throws Throwable {
throw new Throwable("不被忽略");
}
}
Intercepting Invocations
拦截测试方法,类似于 Spring 中的 AOP
@Slf4j
@ExtendWith(MyInvocationInterceptorTest.LogInvocationInterceptor.class)
public class MyInvocationInterceptorTest {
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void showParameterized(String candidate) {
log.error(candidate);
}
static class LogInvocationInterceptor implements InvocationInterceptor {
@Override
public void interceptTestTemplateMethod(Invocation invocation,
ReflectiveInvocationContext invocationContext,
ExtensionContext extensionContext) throws Throwable {
Method executable = invocationContext.getExecutable();
List
InvocationInterceptor 中有多个方法 interceptBeforeAllMethod
interceptTestMethod
interceptTestTemplateMethod
等,分别在不同的时候拦截,里中 @ParameterizedTest
继承 @TestTemplate
所以使用 interceptTestTemplateMethod
拦截器中一般会传入这几个变量:
- invocation: 测试请求,只有
proceed()
代表执行 - invocationContext: 测试请求的上下文
- extensionContext: 扩展的上下文
为 Test Templates 提供上下文
上面提到了 @ParameterizedTest
是由 @TestTemplate
, 而 @TestTemplate
至少需要一个 TestTemplateInvocationContextProvider
提供时执行,在 @ParameterizedTest
中我们可以看到,@ParameterizedTest
由 ParameterizedTestExtension.class
提供测试的参数
@TestTemplate
@ExtendWith(ParameterizedTestExtension.class)
public @interface ParameterizedTest {
// ...
}
所以,相对于我写例子,直接学习它的源码可能更好,这是真实的案例,下面是 ParameterizedTestExtension.class
部分内容
class ParameterizedTestExtension implements TestTemplateInvocationContextProvider {
private static final String METHOD_CONTEXT_KEY = "context";
// 在 TestTemplateInvocationContextProvider 提供两个方法,这是其中一个
// 用于判断是否支持该扩展,例如下面两判断分别是不存在测试方法与不存在注解@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);
Preconditions.condition(methodContext.hasPotentiallyValidSignature(),
() -> String.format(
"@ParameterizedTest method [%s] declares formal parameters in an invalid order: "
+ "argument aggregators must be declared after any indexed arguments "
+ "and before any arguments resolved by another ParameterResolver.",
testMethod.toGenericString()));
getStore(context).put(METHOD_CONTEXT_KEY, methodContext);
return true;
}
// 这是另一个方法
// 提供测试的参数
// 返回一个Stream,简单的样式是 Stream.of(invocationContext("apple"), invocationContext("banana"));
// ParameterizedTestExtension中比较复杂,大概是 获取提供值(获取参数提供器 -> 消费注解) -> 获取并消费参数 -> 构建InvocationContext
@Override
public Stream provideTestTemplateInvocationContexts(ExtensionContext extensionContext) {
Method templateMethod = extensionContext.getRequiredTestMethod();
String displayName = extensionContext.getDisplayName();
ParameterizedTestMethodContext methodContext = getStore(extensionContext)//
.get(METHOD_CONTEXT_KEY, ParameterizedTestMethodContext.class);
ParameterizedTestNameFormatter formatter = createNameFormatter(templateMethod, displayName);
AtomicLong invocationCount = new AtomicLong(0);
// @formatter:off
return findRepeatableAnnotations(templateMethod, ArgumentsSource.class)
.stream()
.map(ArgumentsSource::value)
.map(this::instantiateArgumentsProvider)
.map(provider -> AnnotationConsumerInitializer.initialize(templateMethod, provider))
.flatMap(provider -> arguments(provider, extensionContext))
.map(Arguments::get)
.map(arguments -> consumedArguments(arguments, methodContext))
.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
}
// ...
}
在扩展中保持状态
熟悉前端的知道在 vue 或者 react 中都会涉及到状态 state 的保持,在junit 5 中也提供了类似的API Store
(连名字都差不多。。。),大致上你可以理解为Map这类的东西,在 ParameterizedTestExtension
中也使用它存储了 METHOD_CONTEXT_KEY
- 例子源码:https://github.com/jiangtj-lab/junit5-demo
本文作者: Mr.J
本文链接: https://www.dnocm.com/articles/cherry/junit-5-info/
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!