配色: 字号:
JUnit 5 系列:扩展模型(Extension Model)
2016-09-21 | 阅:  转:  |  分享 
  
JUnit5系列:扩展模型(ExtensionModel)

概述

目录

JUnit4的扩展模型

Runners(运行器)

Rules(规则)

现状

JUnit5的扩展模型

扩展点

无状态

应用扩展

自定义注解

例子

回顾总结

分享&关注

「译者注:本篇的Runner,统一译为“运行器”;Rule,统一译为“规则”。虽不一定完全达义,但语义未损失太多。在每小节第一次出现处会以中英标注,其后全部使用中文。」

JUnit4的扩展模型

我们先来看看JUnit4中是如何实现扩展的。在JUnit4中实现扩展主要是通过两个,有时也互有重叠的扩展机制:运行器(Runners)和规则(Rules)。

运行器(Runners)

测试运行器负责管理诸多测试的生命周期,包括它们的实例化、setup/teardown方法的调用、测试运行、异常处理、发送消息等。在JUnit4提供的运行器实现中,它负责了这所有的事情。

在JUnit4中,扩展JUnit的唯一方法是:创建一个新的运行器,然后使用它标记你新的测试类:@Runwith(MyRunner.class)。这样JUnit就会识别并使用它来运行测试,而不会使用其默认的实现。

这个方式很重,对于小定制小扩展来说很不方便。同时它有个很苛刻的限制:一个测试类只能用一个运行器来跑,这意味着你不能组合不同的运行器。也即是说,你不能同时享受到两个以上运行器提供的特性,比如说不能同时使用Mockito和Spring的运行器,等。

规则(Rules)

为了克服这个限制,JUnit4.7中引入了规则的概念,它是指测试类中特别的注解字段。JUnit4会把测试方法(与一些其他的行为)包装一层传给规则。规则因此可以在测试代码执行前后插入,执行一些代码。很多时候在测试方法中也会直接调规则类上的方法。

这里有一个例子,展示的是?temporaryfolder?(临时文件夹)规则:

publicstaticclassHasTempFolder{

@Rule

publicTemporaryFolderfolder=newTemporaryFolder();



@Test

publicvoidtestUsingTempFolder()throwsIOException{

FilecreatedFile=folder.newFile("myfile.txt");

FilecreatedFolder=folder.newFolder("subfolder");

//...

}

}

因为?@Rule?注解的存在,JUnit会先把测试方法?testUsingTempFolder?包装成一个可执行代码块,传给?folder?规则。这个规则的作用是执行时,由?folder创建一个临时目录,执行测试,测试完成后删除临时目录。因此,在测试内部可以放心地在临时目录下创建文件和文件夹。

当然还有其他的规则,

规则特性其实已经是个很大的改进了,不过仍有局限,它只能在测试运行之前或之后定制操作。如果你想在此之外的时间点进行扩展,这个特性也无能为力了。

现状

总而言之,在JUnit4中存在两种不同的扩展机制,两者均各有局限,并且功能还有重叠的部分。在JUnit4下编写干净的扩展是很难的事。此外,即使你尝试组合两种不同的扩展方式,通常也不会一帆风顺,有时它可能根本不按照开发者期望的方式工作。

JUnit5的扩展模型

JunitLambda项目成立伊始便有几点核心准则,其中一条便是“扩展点优于新特性”。这个准则其实也就是新版本JUnit中最重要的扩展机制了——并非唯一,但无疑是最重要之一。

扩展点

JUnit5扩展可以声明其主要关注的是测试生命周期的哪部分。JUnit5引擎在处理测试时,它会依次检查这些扩展点,并调用每个已注册的扩展。大体来说,这些扩展点出现次序如下:

测试类实例后处理

BeforeAll回调

测试及容器执行条件检查

BeforeEach回调

参数解析

测试执行前

测试执行后

异常处理

AfterEach回调

AfterAll回调

(如果上面有你觉得不甚清晰或理解的点,请不用担心,我们接下来会挑其中的一些来讲解。)

每个扩展点都对应一个接口。接口方法会接受一些参数,一些扩展点所处生命周期的上下文信息。比如,被测实例与方法、测试的名称、参数、注解等信息。

一个扩展可以实现任意个以上的接口方法,引擎会在调用它们时传入相应的上下文信息作为参数。有了这些信息,扩展就可以放心地实现所需的功能了。

无状态

这里我们需要考虑一个重要的细节:引擎对扩展实例的初始化时间、实例的生存时间未作出任何规约和保证,因此,扩展必须是无状态的。如果一个扩展需要维持任何状态信息,那么它必须使用JUnit提供的一个仓库(store)来进行信息读取和写入。

这样做的原因有几个:

扩展的初始化时机和方式对引擎是未知的(每个测试实例化一次?每个类实例化一次?还是每次运行实例化一次?)。

JUnit不想额外维护和管理每个扩展创建的实例。

如果扩展之间想要进行通信,那么无论如何JUnit都必须提供一个数据交互的机制。

应用扩展

创建完扩展后,接下来需要做的就仅仅是告诉JUnit它的存在。这可以通过在需要使用该扩展的测试类或测试方法上添加一个?@ExtendWith(MyExtension.class)?简单实现。

其实,还有另一种更简明的方式。不过要理解那种方式,我们必须先看一下JUnit的扩展模型中还有哪些内容。

自定义注解

JUnit5的API大部分是基于注解的,而且引擎在检查注解时还做了些额外的工作:它不仅会查找字段、类、参数上应用的注解,还会注解上的注解。引擎会把找到的所有注解都应用到被注解元素上。注解另一个注解可以通过所谓的元注解做到,酷的是Junit提供的所有注解都说得上是元注解了。

它的意义在于,JUnit5中我们就能够创建并组合不同的注解了,并且它们具备组合多个注解特性的能力:



/

Wedefineacustomannotationthat:

-standsinfor''@Test''sothatthemethodgetsexecuted

-hasthetag"integration"sowecanfilterbythat,

e.g.whenrunningtestsfromthecommandline

/

@Target({ElementType.TYPE,ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Test

@Tag("integration")

public@interfaceIntegrationTest{}

这个自定义的“集成测试”注解?@IntegrationTest?可以这样使用:

@IntegrationTest

voidrunsWithCustomAnnotation(){

//thisgetsexecuted

//eventhough`@IntegrationTest`isnotdefinedbyJUnit

}

进一步我们可以为扩展使用更简明的注解:

@Target({ElementType.TYPE,ElementType.METHOD,ElementType.ANNOTATION_TYPE})

@Retention(RetentionPolicy.RUNTIME)

@ExtendWith(ExternalDatabaseExtension.class)

public@interfaceDatabase{}

现在我们可以直接使用?@Database?注解了,而不需要再声明测试应用了特定的扩展?@ExtendWith(ExternalDatabaseExtension.class)。并且由于我们把注解类型?ElementType.ANNOTATION_TYPE?也添加到扩展支持的目标类型中去了,因此该注解也可以被我们或他人进一步的使用、组合。

例子

假设现在有个场景,我想量化一下测试运行花费的时间。首先,可以先创建一个我们想要的注解:

@Target({TYPE,METHOD,ANNOTATION_TYPE})

@Retention(RetentionPolicy.RUNTIME)

@ExtendWith(BenchmarkExtension.class)

public@interfaceBenchmark{www.visa158.com}

注解声明其应用了?BenchmarkExtension?扩展,这是我们接下来要实现的。TODOLIST如下:

计算所有测试类的运行时间,在所有测试执行前保存其起始时间

计算每个测试方法的运行时间,在每个测试方法执行前保存其起始时间

在每个测试方法执行完毕后,获取其结束时间,计算并输出该测试方法的运行时间

在所有测试类执行完毕后,获取其结束时间,计算并输出所有测试的运行时间

以上操作,仅对所有注解了?@BenchMark?的测试类或测试方法生效

最后一点需求可能不是一眼便能发现。如果一个方法并未注解?@Benchmark?注解,它有什么可能被我们的扩展处理?一个语法上的原因是,如果一个扩展被应用到了一个类上,那么它默认也会应用到类中的所有方法上。因此,如果我们的需求是计算整个测试类的运行时间,但不需具体到类中每个单独方法的运行时间时,类中的测试方法就必须被手动排除。这点我们可以通过单独检查每个方法是否应用了注解来做到。

有趣的是,需求的前四点与扩展点中的其中四个是一一对应的:BeforeAll、BeforeTestExecution、AfterTestExecution?与?AfterAll。因此我们要做的任务便是实现这四个对应的接口。具体实现很简单,把上面说的翻译成代码即是:

publicclassBenchmarkExtensionimplements

BeforeAllExtensionPoint,BeforeTestExecutionCallback,

AfterTestExecutionCallback,AfterAllExtensionPoint{



privatestaticfinalNamespaceNAMESPACE=

Namespace.of("BenchmarkExtension");



@Override

publicvoidbeforeAll(ContainerExtensionContextcontext){

if(!shouldBeBenchmarked(context))

return;



writeCurrentTime(context,LaunchTimeKey.CLASS);

}



@Override

publicvoidbeforeTestExecution(TestExtensionContextcontext){

if(!shouldBeBenchmarked(context))

return;



writeCurrentTime(context,LaunchTimeKey.TEST);

}



@Override

publicvoidafterTestExecution(TestExtensionContextcontext){

if(!shouldBeBenchmarked(context))

return;



longlaunchTime=loadLaunchTime(context,LaunchTimeKey.TEST);

longruntime=currentTimeMillis()-launchTime;

print("Test",context.getDisplayName(),runtime);

}



@Override

publicvoidafterAll(ContainerExtensionContextcontext){

if(!shouldBeBenchmarked(context))

return;



longlaunchTime=loadLaunchTime(context,LaunchTimeKey.CLASS);

longruntime=currentTimeMillis()-launchTime;

print("Testcontainer",context.getDisplayName(),runtime);

}



privatestaticbooleanshouldBeBenchmarked(ExtensionContextcontext){

returncontext.getElement()

.map(el->el.isAnnotationPresent(Benchmark.class))

.orElse(false);

}



privatestaticvoidwriteCurrentTime(

ExtensionContextcontext,LaunchTimeKeykey){

context.getStore(NAMESPACE).put(key,currentTimeMillis(www.hunanwang.net));

}



privatestaticlongloadLaunchTime(

ExtensionContextcontext,LaunchTimeKeykey){

return(Long)context.getStore(NAMESPACE).remove(key);

}



privatestaticvoidprint(

Stringunit,StringdisplayName,longruntime){

System.out.printf("%s''%s''took%dms.%n",unit,displayName,runtime);

}



privateenumLaunchTimeKey{

CLASS,TEST

}

}



「译者:啊这代码让人心旷神怡。」

上面代码有几个地方值得留意。首先是?shouldBeBenchmarked?方法,它使用了JUnit的API来获取当前元素是否(被元)注解了?@Benchmark?注解;其次,?writeCurrentTime?/?loadLaunchTime?方法中使用了Junit提供的store以写入和读取运行时间。

源代码在Github上可以找到。

下篇博文我会探讨条件执行的测试以及参数注入部分的内容,同时为你展示如何使用其对应的扩展点。如果你已经迫不及待了,那么请先参考这篇博客,它展示了将应用了两个规则(条件性禁用测试及临时目录)的Junit4测试改装成JUnit5测试的方法。

总结回顾

通过本文我们了解到,在创建整洁、强大及可组合的扩展上,JUnit4提供的运行器和规则特性不够理想。为了超越这些限制,JUnit5引入了一个更通用的概念:扩展点。它允许自定义的扩展主动声明,它需要在一个测试的什么节点上去介入。同时,我们还看到如何使用元注解来轻松地自定义注解。

我希望听到你的想法和反馈。



献花(0)
+1
(本文系白狐一梦首藏)