![fe8da592ce6fd42041aaf14c60c4b0d5.png](https://i-blog.csdnimg.cn/blog_migrate/cb731b9b76b8002a48da675e036ab4e7.png)
前言
在org.junit.jupiter.api.extension
包下,JUnit
5提供了丰富的扩展接口,通过实现这些接口,我们可以定制自己的扩展并注册到JUnit
中来实现功能扩展。
Extension接口
这个接口可以说是JUnit
扩展中最为重要的接口,不过与它的重要性不相匹配的是,它没有定义任何的成员:
public interface Extension { }
然而它的子接口却极其丰富:
![8209ac4609124e8dd9d7a64e10891552.png](https://i-blog.csdnimg.cn/blog_migrate/d48475807ec93e7cc9138a4d8af81f9f.jpeg)
注册扩展的方式
前面说过,我们可以通过实现Extension
的子接口来定制自己的扩展,那么注册又是如何做到的呢?在JUnit
5中提供了两个注解来帮助我们将自定义扩展注册到JUnit
,它们分别是:@ExtendWith
和@RegisterExtension
。
- @ExtendWith
这个注解可标注在类型或方法上,通过声明式的方式注册扩展。是可重复注解的同时,又接受一个Extension
类型的数组,当存在多个扩展时,你可在数组中传入多个Extension
,抑或是结合@Extensions
注解使用:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(Extensions.class)
public @interface ExtendWith {
Class<? extends Extension>[] value();
}
- @RegisterExtension
这个注解也是用来注册扩展的,但是它只能被用到字段上,且相对于@ExtendWith
的声明式注册方式,它是编程式的注册方式。被它标注的字段可以是static
或non-static
,但这个字段不能被private
修饰,实际的值也不能为null
:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RegisterExtension {
}
另外,对于被static
修饰和不被static
修饰也要分情况讨论:
static
:通过这种方式注册的扩展不受任何限制,你可以注册BeforeAllCallback
、AfterAllCallback
、TestInstanceFactory
、TestInstancePostProcessor
、TestInstancePreDestroyCallback
、BeforeEachCallback
等等类型。non-static
:这种实例类型的注册往往会被延迟,它会在测试类实例化完成并且方法级别的@ExtendWith
都注册之后,这回导致一些类级别或实例级别的扩展如:BeforeAllCallback
、AfterAllCallback
、TestInstanceFactory
、TestInstancePostProcessor
的注册出现问题。但是,如果你在测试类上标注了@TestInstance(Lifecycle.PER_CLASS)
,那么它将会被提前到方法级别的@ExtendWith
注册之前注册。
@RegisterExtension
标注的字段注册顺序是根据算法确定的,如果你想要明确他们的注册顺序,可以使用org.junit.jupiter.api.Order
注解指定。
声明式 Vs 编程式
现在有必要考虑一个问题:声明式的注册意味着什么?为什么需要编程式的注册?
首先声明式注册的扩展都不支持手动调用扩展类的构造方法或工厂方法等传入参数,意味着它们的行为将无法定制,因而存在一定的局限性;由此才引出了编程式的注册方法,我们可以通过扩展的构造器或者工厂方法等传入参数,从而实现进一步的扩展定制!
这样一来我们便可知道,声明式有它的便利之处,而编程式也有它的强大之处,没有孰优孰劣,都有自己的适用场景。
注册示例
JUnit
支持我们对异常做扩展,一个测试方法中可能会抛出异常,而这异常会被JUnit
捕获。通过Extension
下的子接口TestExecutionExceptionHandler
,我们可以决定在捕获异常之后的行为。
由于@ExtendWith
使用简单,且在其它文章中已多次用到,这里就不再赘述,只讲@RegisterExtension
。先在测试类中创建一个ExceptionHandler
:
static class ExceptionHandler implements TestExecutionExceptionHandler {
private String message;
ExceptionHandler(String message) {
this.message = message;
}
@Override
public void handleTestExecutionException(
ExtensionContext context, Throwable throwable) throws Throwable {
throw new RuntimeException(String.format(
"Display name: [%s], reason: [%s], raw message: [%s]",
context.getDisplayName(), this.message, throwable.getMessage()));
}
}
之后通过字段方式注册它,同时写一个普通的测试方法,抛出一个异常:
class JunitSampleTests {
static class ExceptionHandler implements TestExecutionExceptionHandler {
// ...
}
@RegisterExtension
static ExceptionHandler exceptionHandler = new ExceptionHandler("异常被捕获了");
@Test
void test() {
throw new RuntimeException("测试出错啦!");
}
}
注意上面调用了ExceptionHandler
的构造器传入了message
,这是声明式的@ExtendWith
无法做到的:
![3ac6bff9bcb6fe50086309664818a17a.png](https://i-blog.csdnimg.cn/blog_migrate/900d4f0583a0dfd2983934c6658538be.jpeg)
SPI注册
不知你是否了解过Java
中的SPI(Service Provider Interface)
机制,它可以用来启用框架扩展和替换组件,这在JUnit
中是支持的,借此我们可以在不使用注解的情况下注册我们的扩展。
如果你是Maven
工程,需要在test
目录下的测试资源目录resources
(这个目录一般不存在,需要手动创建并右键标记为“Test Resources Root
”)中新建一个名为junit-platform.properties
的文件(这个文件名不能随意,否则JUnit
无法扫描到),并在其中加上配置:
junit.jupiter.extensions.autodetection.enabled=true
而后同样在这个resources
下,新建目录WEB-INF/services
,之后创建一个名为org.junit.jupiter.api.extension.Extension
的文件,这个文件名其实就是Extension
接口的全限定类名,而其中的内容就是我们实现的扩展类的全限定类名(如有多个扩展实现,一个限定名占一行):
com.tinysand.fileuploads.ExceptionHandler
这里我把之前的扩展实现单独提成了一个类文件,同时去掉了带参的构造器和成员,显然这种方式也只能像@ExtendWith
注解做的那样。
最后,如果不想使用配置文件,你可以在VM
(Run/Debug Configurations
中的VM options
)参数添加如下参数而达到同样的效果:
-Djunit.jupiter.extensions.autodetection.enabled=true
软件版本
软件 | 版本 |
---|---|
JUnit | 5.6.2(junit-jupiter) |