OverviewEnvironmentConstructionBasicEntryArchitectureSystemExtensionModel(ExtensionModel)ConditionalAssertionInjectionDynamicTest...(不喜欢看文章的可以点这里看我的演讲,或者看看最近的vJUG讲座,或者我在DevoxxPL上的PPT。本系列文章基于Milestone2withJunit5的预发布版本。它可能会发生变化。如果发布新的里程碑(milestone)版本,或者试试等JUnit5版本正式发布的时候再更新这篇文章,这里要介绍的大部分知识都可以在JUnit5UserGuide中找到(这个链接指向Milestone2的第一个版本,如果你想查看最新版本的文档,请点击这里),指南还有更多内容等你去发现。下面所有代码都可以在我的Github上找到。目录JUnit4扩展模型*Runners(跑步者)*Rules(规则)*状态JUnit5扩展模型*扩展点*无状态*应用扩展*自定义注解示例回顾总结Share&follow可能表达不完整,但是语义也没有丢失太多。每个部分的第一次出现将用中文和英文标注,之后所有的都将使用中文。”JUnit4的扩展模型下面我们来看看JUnit4中是如何实现扩展的。扩展在JUnit4中主要通过两个有时重叠的扩展机制实现:Runners和Rules。Runners测试运行器负责管理许多测试周期的生命周期,包括它们的实例化、setup/teardown方法的调用、测试运行、异常处理、发送消息等。在JUnit4提供的运行器实现中,它负责所有这些。在JUnit4中,扩展JUnit的唯一方法是:创建一个新的运行器,然后用它来标记你的新测试类:@Runwith(MyRunner.class)。这样JUnit就会识别并使用它来运行测试,而不是使用它的默认实现。这种方法非常繁重,对于小的自定义和小的扩展来说非常不方便。同时,它有一个非常严格的限制:一个测试类只能跑一个runner,也就是说不能组合不同的runner。也就是说不能同时享受两个以上runner提供的特性,比如不能同时使用Mockito和Springrunner等。规则(Rules)为了克服这个限制,JUnit4.7引入了规则的概念,指的是测试类中的特殊注解字段。JUnit4会将测试方法(和一些其他行为)包装到规则中。因此可以在测试代码执行之前和之后插入规则,执行一些代码。很多时候,规则类上的方法也是在测试方法中直接调用的。下面是一个显示临时文件夹规则的示例:...}}因为@Rule注解,JUnit会先将测试方法testUsingTempFolder包装成一个可执行代码块,然后传递给folderrule。该规则的作用是在执行过程中按文件夹创建临时目录,执行测试,测试完成后删除临时目录。因此,在测试内部的临时目录下创建文件和文件夹是安全的。当然还有其他规则,例如允许您在Swing的事件分派线程中执行测试的规则、负责连接和断开数据库的规则以及简单地使运行时间过长的测试超时的规则。规则功能实际上是一个很大的改进,但仍然有限,它只能在测试运行之前或之后自定义操作。如果你想扩展到这个时间点之后,这个功能就帮不上忙了。现状总之,JUnit4中有两种不同的扩展机制,它们都有局限性并且在功能上有重叠。在JUnit4下编写干净的扩展是一项艰巨的工作。此外,即使您尝试组合两个不同的扩展,它通常也不会起作用,有时它可能根本无法按照开发人员的预期方式起作用。JUnit5的扩展模型JunitLambda项目是根据几个核心原则建立的,其中之一是“新功能的扩展点”。这个标准实际上是新版JUnit中最重要的扩展机制——不是唯一的,但无疑是最重要的之一。扩展点JUnit5扩展可以声明它主要关注测试生命周期的哪一部分。当JUnit5引擎处理测试时,它依次检查这些扩展点,调用每个已注册的扩展。一般来说,这些扩展点按以下顺序出现:测试类实例后处理BeforeAll回调测试和容器执行条件检查BeforeEach回调参数分析测试执行前测试执行后AfterEach回调AfterAll回调理解了重点就别着急,我们接下来会挑选其中的一些来进行解释。)每个扩展点对应一个接口。接口方法会接受一些参数和一些扩展点的生命周期的上下文信息。例如被测试的实例和方法、测试的名称、参数、注解等信息。一个扩展可以实现任意数量的接口方法,引擎在调用它们时会传入相应的上下文信息作为参数。有了这些信息,扩展就可以自信地实现所需的功能。Stateless这里我们需要考虑一个重要的细节:引擎对扩展实例的初始化时间和生命周期没有做任何规定或保证。因此,扩展必须是无状态的。如果扩展需要维护任何状态信息,它必须使用JUnit提供的存储来读取和写入信息。这有几个原因:引擎不知道何时以及如何初始化扩展(每个测试一次?每个类一次?每次运行一次?)。JUnit不想额外维护和管理每个扩展创建的实例。如果扩展想要通信,那么JUnit无论如何都必须提供数据交换机制。应用扩展创建扩展后,您需要做的就是告诉JUnit它存在。这可以通过在需要使用扩展的测试类或测试方法上添加@ExtendWith(MyExtension.class)来轻松实现。其实还有一种更简洁的方式。但是要理解这种方法,我们必须首先看看JUnit的扩展模型中还有什么。自定义注解JUnit5的API主要是基于注解的,引擎在检查注解时会做一些额外的工作:它不仅会查找应用于字段、类、参数的注解,还会查找注解。引擎会将它找到的所有注释应用于带注释的元素。注解另一个注解可以用所谓的元注解来完成,很酷的是Junit提供的所有注解都可以称为元注解。它的意义在于,在JUnit5中我们可以创建和组合不同的注解,它们具有组合多个注解特性的能力:当从命令行运行测试时*/@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Test@Tag("integration")public@interfaceIntegrationTest{}这个自定义的“集成测试”注释@IntegrationTest可以像这样使用this:@IntegrationTestvoidrunsWithCustomAnnotation(){//thisgetsexecuted//eventthough`@IntegrationTest`isnotdefinedbyJUnit}此外,我们可以为扩展使用更简洁的注释:@Target({ElementType.TYPE,ElementType.METHOD,ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@ExtendWith(ExternalDatabaseExtension.class)public@interfaceDatabase{}现在我们可以直接使用@Database注解而不用声明测试应用特定的extension@ExtendWith(ExternalDatabaseExtension.class)。并且由于我们将注解类型ElementType.ANNOTATION_TYPE添加到扩展支持的目标类型中,因此该注解也可以被我们或其他人进一步使用和组合。示例假设有一个场景,我想量化测试运行所需的时间。首先,我们可以创建一个我们想要的注解:@Target({TYPE,METHOD,ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@ExtendWith(BenchmarkExtension.class)public@interfaceBenchmark{}注解声明它应用BenchmarkExtension扩展,这个是我们接下来要实现的目标。TODOLIST如下:计算所有测试类的运行时间,并在所有测试执行前保存它们的启动时间计算每个测试方法的运行时间,并在每个测试方法执行前保存其启动时间每个测试方法执行完毕后,得到结束时间,计算并输出测试方法的运行时间所有测试类执行完毕后,获取结束时间,计算并输出所有测试的运行时间以上操作仅针对所有带@注解的测试类BenchMarkorThetestmethodtakeseffect***一点点需求可能一眼看不出来。如果一个方法没有使用@Benchmark注解,它被我们的扩展处理的可能性有多大?一个语法原因是,如果扩展应用于类,默认情况下它也将应用于类中的所有方法。.因此,如果我们的需求是计算整个测试类的运行时间,而不是具体到类中每个单独方法的运行时间,则必须手动排除类中的测试方法。如果应用了注释,我们可以通过单独检查每个方法来做到这一点。有趣的是,需求的前四个点与扩展点中的四个点之间存在一对一的对应关系:BeforeAll、BeforeTestExecution、AfterTestExecution和AfterAll。因此,我们要做的任务就是实现这四个对应的接口。具体实现很简单,把上面说的翻译成代码即是:publicclassBenchmarkExtensionimplementsBeforeAllExtensionPoint,BeforeTestExecutionCallback,AfterTestExecutionCallback,AfterAllExtensionPoint{privatestaticfinalNamespaceNAMESPACE=Namespace.of("BenchmarkExtension");@OverridepublicvoidbeforeAll(ContainerExtensionContextcontext){if(!shouldBeBenchmarked(context))return;writeCurrentTime(context,LaunchTimeKey.CLASS);}@OverridepublicvoidbeforeTestExecution(TestExtensionContextcontext){if(!shouldBeBenchmarked(context))返回;writeCurrentTime(context,LaunchTimeKey.TEST);}@OverridepublicvoidafterTestExecution(TestExtensionContextcontext){if(!shouldBeBenchmarked(context))返回;longlaunchTime=loadLaunchTime(上下文,LaunchTimeKey.TEST);longruntime=currentTimeMillis()-launchTime;打印(“测试”,context.getDisplayName(),运行时);}@OverridepublicvoidafterAll(ContainerExtensionContextcontext){if(!shouldBeBenchmarked(上下文))return;longlaunchTime=loadLaunchTime(上下文,启动TimeKey.CLASS);longruntime=currentTimeMillis()-launchTime;print("Testcontainer",context.getDisplayName(),runtime);}privatestaticbooleanshouldBeBenchmarked(ExtensionContextcontext){returncontext.getElement().map(el->el.isAnnotationPresent(基准.class)).orElse(false);}privatestaticvoidwriteCurrentTime(ExtensionContextcontext,LaunchTimeKeykey){context.getStore(NAMESPACE).put(key,currentTimeMillis());}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方法,它使用JUnitAPI来获取当前元素是否被(元)注释了@Benchmark注解;其次,writeCurrentTime/loadLaunchTime方法使用Junit提供的存储来写入和读取运行时间。源代码可在Github上获得。在下一篇博文中,我将讨论条件执行的测试和参数注入的内容,并展示如何使用其对应的扩展点。如果你等不及,请先参考这篇博客,它展示了改造Junit4测试的方法,将两个规则(条件禁用测试和临时目录)应用于JUnit5测试。总结回顾通过这篇文章,我们了解到JUnit4提供的运行器和规则功能对于创建干净、强大且可组合的扩展来说并不理想。为了超越这些限制,JUnit5引入了一个更通用的概念:扩展点。它允许自定义扩展主动声明在测试中需要干预哪些节点。此外,我们还看到了如何使用元注释轻松自定义注释。我很想听听您的想法和反馈。
