原文地址:http://blog.codefx.org/design/architecture/junit-5-extension-model/

原文日期:11, Apr, 2016

译文首发:Linesh 的博客:「译」JUnit 5 系列:扩展模型(Extension Model)

我的 Github:http://github.com/linesh-simplicity

概述

(如果不喜欢看文章,你可以戳这里看我的演讲,或者看一下最近的 vJUG 讲座,或者我在 DevoxxPL 上的 PPT

本系列文章都基于 Junit 5发布的先行版 Milestone 2。它可能会有变化。如果有新的里程碑(milestone)版本发布,或者试用版正式发行时,我会再来更新这篇文章。

这里要介绍的多数知识你都可以在 JUnit 5 用户指南 中找到(这个链接指向的是先行版 Milestone 2,想看的最新版本文档的话请戳这里),并且指南还有更多的内容等待你发掘。下面的所有代码都可以在 我的 Github 上找到。

目录

  • JUnit 4 的扩展模型

    • Runners(运行器)
    • Rules(规则)
    • 现状
  • JUnit 5 的扩展模型
    • 扩展点
    • 无状态
    • 应用扩展
    • 自定义注解
  • 例子
  • 回顾总结
  • 分享&关注

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

JUnit 4 的扩展模型

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

运行器(Runners)

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

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

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

规则(Rules)

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

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

  1. public static class HasTempFolder {
  2. @Rule
  3. public TemporaryFolder folder= new TemporaryFolder();
  4. @Test
  5. public void testUsingTempFolder() throws IOException {
  6. File createdFile= folder.newFile("myfile.txt");
  7. File createdFolder= folder.newFolder("subfolder");
  8. // ...
  9. }
  10. }

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

当然还有其他的规则,比如允许你在 Swing 的事件分发线程中执行测试 的规则,负责连接和断开数据库的规则,以及让运行过久的测试直接超时的规则等。

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

现状

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

JUnit 5 的扩展模型

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

扩展点

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

  • 测试类实例 后处理
  • BeforeAll 回调
  • 测试及容器执行条件检查
  • BeforeEach 回调
  • 参数解析
  • 测试执行前
  • 测试执行后
  • 异常处理
  • AfterEach 回调
  • AfterAll 回调

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

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

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

无状态

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

这样做的原因有几个:

  • 扩展的初始化时机和方式对引擎是未知的(每个测试实例化一次?每个类实例化一次?还是每次运行实例化一次?)。
  • JUnit 不想额外维护和管理每个扩展创建的实例。
  • 如果扩展之间想要进行通信,那么无论如何 JUnit 都必须提供一个数据交互的机制。

应用扩展

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

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

自定义注解

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

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


  1. /**
  2. * We define a custom annotation that:
  3. * - stands in for '@Test' so that the method gets executed
  4. * - has the tag "integration" so we can filter by that,
  5. * e.g. when running tests from the command line
  6. */
  7. @Target({ElementType.TYPE, ElementType.METHOD})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Test
  10. @Tag("integration")
  11. public @interface IntegrationTest { }

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

  1. @IntegrationTest
  2. void runsWithCustomAnnotation() {
  3. // this gets executed
  4. // even though `@IntegrationTest` is not defined by JUnit
  5. }

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

  1. @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @ExtendWith(ExternalDatabaseExtension.class)
  4. public @interface Database { }

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

例子

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

  1. @Target({ TYPE, METHOD, ANNOTATION_TYPE })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @ExtendWith(BenchmarkExtension.class)
  4. public @interface Benchmark { }

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

  • 计算所有测试类的运行时间,在所有测试执行前保存其起始时间
  • 计算每个测试方法的运行时间,在每个测试方法执行前保存其起始时间
  • 在每个测试方法执行完毕后,获取其结束时间,计算并输出该测试方法的运行时间
  • 在所有测试类执行完毕后,获取其结束时间,计算并输出所有测试的运行时间
  • 以上操作,仅对所有注解了 @BenchMark 的测试类或测试方法生效

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

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

  1. public class BenchmarkExtension implements
  2. BeforeAllExtensionPoint, BeforeTestExecutionCallback,
  3. AfterTestExecutionCallback, AfterAllExtensionPoint {
  4. private static final Namespace NAMESPACE =
  5. Namespace.of("BenchmarkExtension");
  6. @Override
  7. public void beforeAll(ContainerExtensionContext context) {
  8. if (!shouldBeBenchmarked(context))
  9. return;
  10. writeCurrentTime(context, LaunchTimeKey.CLASS);
  11. }
  12. @Override
  13. public void beforeTestExecution(TestExtensionContext context) {
  14. if (!shouldBeBenchmarked(context))
  15. return;
  16. writeCurrentTime(context, LaunchTimeKey.TEST);
  17. }
  18. @Override
  19. public void afterTestExecution(TestExtensionContext context) {
  20. if (!shouldBeBenchmarked(context))
  21. return;
  22. long launchTime = loadLaunchTime(context, LaunchTimeKey.TEST);
  23. long runtime = currentTimeMillis() - launchTime;
  24. print("Test", context.getDisplayName(), runtime);
  25. }
  26. @Override
  27. public void afterAll(ContainerExtensionContext context) {
  28. if (!shouldBeBenchmarked(context))
  29. return;
  30. long launchTime = loadLaunchTime(context, LaunchTimeKey.CLASS);
  31. long runtime = currentTimeMillis() - launchTime;
  32. print("Test container", context.getDisplayName(), runtime);
  33. }
  34. private static boolean shouldBeBenchmarked(ExtensionContext context) {
  35. return context.getElement()
  36. .map(el -> el.isAnnotationPresent(Benchmark.class))
  37. .orElse(false);
  38. }
  39. private static void writeCurrentTime(
  40. ExtensionContext context, LaunchTimeKey key) {
  41. context.getStore(NAMESPACE).put(key, currentTimeMillis());
  42. }
  43. private static long loadLaunchTime(
  44. ExtensionContext context, LaunchTimeKey key) {
  45. return (Long) context.getStore(NAMESPACE).remove(key);
  46. }
  47. private static void print(
  48. String unit, String displayName, long runtime) {
  49. System.out.printf("%s '%s' took %d ms.%n", unit, displayName, runtime);
  50. }
  51. private enum LaunchTimeKey {
  52. CLASS, TEST
  53. }
  54. }
  55. 「译者:啊这代码让人心旷神怡。」

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

源代码在 Github 上可以找到。

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

总结回顾

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

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


「译」JUnit 5 系列:扩展模型(Extension Model)的更多相关文章

  1. 「译」JUnit 5 系列:条件测试

    原文地址:http://blog.codefx.org/libraries/junit-5-conditions/ 原文日期:08, May, 2016 译文首发:Linesh 的博客:「译」JUni ...

  2. 「译」JUnit 5 系列:环境搭建

    原文地址:http://blog.codefx.org/libraries/junit-5-setup/ 原文日期:15, Feb, 2016 译文首发:Linesh 的博客:环境搭建 我的 Gith ...

  3. 「译」JUnit 5 系列:架构体系

    原文地址:http://blog.codefx.org/design/architecture/junit-5-architecture/ 原文日期:29, Mar, 2016 译文首发:Linesh ...

  4. 「译」JUnit 5 系列:基础入门

    原文地址:http://blog.codefx.org/libraries/junit-5-basics/ 原文日期:25, Feb, 2016 译文首发:Linesh 的博客:JUnit 5 系列: ...

  5. jvm系列(十):如何优化Java GC「译」

    本文由CrowHawk翻译,是Java GC调优的经典佳作. 本文翻译自Sangmin Lee发表在Cubrid上的"Become a Java GC Expert"系列文章的第三 ...

  6. jvm系列(七):如何优化Java GC「译」

    本文由CrowHawk翻译,地址:如何优化Java GC「译」,是Java GC调优的经典佳作. Sangmin Lee发表在Cubrid上的”Become a Java GC Expert”系列文章 ...

  7. 「译」JavaScript 的怪癖 1:隐式类型转换

    原文:JavaScript quirk 1: implicit conversion of values 译文:「译」JavaScript 的怪癖 1:隐式类型转换 译者:justjavac 零:提要 ...

  8. iOS 9,为前端世界都带来了些什么?「译」 - 高棋的博客

    2015 年 9 月,Apple 重磅发布了全新的 iPhone 6s/6s Plus.iPad Pro 与全新的操作系统 watchOS 2 与 tvOS 9(是的,这货居然是第 9 版),加上已经 ...

  9. 「译」forEach循环中你不知道的3件事

    前言 本文925字,阅读大约需要7分钟. 总括: forEach循环中你不知道的3件事. 原文地址:3 things you didn't know about the forEach loop in ...

随机推荐

  1. C#创建dll类库

    类库让我们的代码可复用,我们只需要在类库中声明变量一次,就能在接下来的过程中无数次地使用,而无需在每次使用前都要声明它.这样一来,就节省了我们的内存空间.而想要在类库添加什么类,还需取决于类库要实现哪 ...

  2. node.js学习(二)--Node.js控制台(REPL)&&Node.js的基础和语法

    1.1.2 Node.js控制台(REPL) Node.js也有自己的虚拟的运行环境:REPL. 我们可以使用它来执行任何的Node.js或者javascript代码.还可以引入模块和使用文件系统. ...

  3. js数组去重几种思路

    在一些后台语言中都内置了一些方法来处理数组或集合中重复的数据.但是js中并没有类似的方法,网上已经有一些方法,但是不够详细.部分代码来源于网络.个人总计如下:大致有4种思路 1)使用两次循环比较原始的 ...

  4. webpack学习总结

    前言 在还未接触webpack,就有几个疑问: 1. webpack本质上是什么? 2. 跟异步模块加载有关系吗? 3. 可否生成多个文件,一定是一个? 4. 被引用的文件有其他异步加载模块怎么办? ...

  5. “风投云涌”:那些被资本看中的IT企业的风光与辛酸

         进入七月份以来,纷享销客获得D轮融资1亿美元,撼动业界,资本与IT联姻令一部分创业者眼红的同时,没有人注意到背后的风险. 科技与资本的结合,是当今经济社会前行的宏大主题.相关统计显示,软件行 ...

  6. jQuery标准的AJAX模板

    $('#saveInformationTemplate_button').on('click', function(){ if(isEmpty($("#name").val())) ...

  7. 从零自学Hadoop(22):HBase协处理器

    阅读目录 序 介绍 Observer操作 示例下载 系列索引 本文版权归mephisto和博客园共有,欢迎转载,但须保留此段声明,并给出原文链接,谢谢合作. 文章是哥(mephisto)写的,Sour ...

  8. [Django]用户权限学习系列之User权限基本操作指令

    针对Django 后台自带的用户管理系统,虽说感觉还可以,但是为了方便用户一些操作,特别设计自定义的用户权限管理系统. 在制作权限页面前,首先需要了解权限和用户配置权限的指令,上章讲到权限的添加,删除 ...

  9. 魅力 .NET:从 Mono、.NET Core 说起

    前段时间,被问了这样一个问题:.NET 应用程序是怎么运行的? 当时大概愣了好久,好像也没说出个所以然,得到的回复是:这是 .NET 程序员最基本的...呵呵! 微软开源,其实不只是对 .NET 本身 ...

  10. Egret3D研究报告(二)从Unity3D导出场景到Egret3D

    Egret3D暂时没有场编的计划,但是我们知道unity3D是一个很好的场编. 有一些游戏即使不是使用Unity3D开发,也使用Unity3D做场编.这里就不点名了,而且并不在少数. 我们就这么干. ...