JUnit 5 简介

2019年12月06日 阅读数:270
这篇文章主要向大家介绍JUnit 5 简介,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

转自:https://www.ibm.com/developerworks/cn/java/j-introducing-junit5-part1-jupiter-api/index.htmlhtml

      https://www.ibm.com/developerworks/cn/java/j-introducing-junit5-part2-vintage-jupiter-extension-model/index.htmljava

第 1 部分git

JUnit 5 Jupiter API

了解全新 JUnit Jupiter API 中的注解、断言和前置条件github

本教程介绍 JUnit 5。咱们首先介绍如何在您的计算机上安装并设置 JUnit 5。我将简要介绍 JUnit 5 的架构和组件,而后展现如何使用 JUnit Jupiter API 中的新注解、断言和前置条件。apache

第 2 部分中,咱们将更深刻地介绍 JUnit 5,包括新的 JUnit Jupiter 扩展模型、参数注入、动态测试等。api

在本教程中,我使用了 JUnit 5, Milestone 5数组

前提条件

出于本教程的目的,我假设您熟悉如下软件的使用:安全

  • Eclipse IDE
  • Maven
  • Gradle(可选)
  • Git

要跟随示例进行操做,您应在计算机上安装 JDK 八、Eclipse、Maven、Gradle(可选)和 Git。若是缺乏其中的任何工具,可以使用下面的连接下载和安装它们:架构

术语

人们倾向于将术语 JUnit 5 和 JUnit Jupiter 看成同义词使用。在大部分状况下,这种互换使用没有什么问题。可是,必定要认识到这两个术语是不一样的。JUnit Jupiter 是使用 JUnit 5 编写测试内容的 API。JUnit 5 是一个项目名称(和版本),其 3 个主要模块关注不一样的方面:JUnit Jupiter、JUnit Platform 和 JUnit Vintage。

当我说起 JUnit Jupiter 时,指的是编写单元测试的 API;说起 JUnit 5 时,指的是整个项目。

JUnit 5 概述

之前的 JUnit 版本都是总体式的。除了在 4.4 版中包含 Hamcrest JAR,JUnit 基原本讲就是一个很大的 JAR 文件。测试内容编写者 — 像您我这样的开发人员 — 和工具供应商都使用它的 API,但后者使用不少内部 JUnit API。

大量使用内部 API 给 JUnit 的维护者形成了一些麻烦,而且留给他们推进该技术发展的选择余地很少。来自 JUnit 5 用户指南

在 JUnit 4 中,只有外部扩展编写者和工具构建者才使用最初做为内部结构而添加的许多功能。这让更改 JUnit 4 变得特别困难,有时甚至根本不可能。

JUnit Lambda(如今称为 JUnit 5)团队决定将 JUnit 从新设计为两个明确且不一样的关注区域:

  • 一个是编写测试内容的 API。
  • 一个是发现和运行这些测试的 API。

这些关注区域如今已整合到 JUnit 5 的架构中,而且它们是明确分离的。图 1 演示了新架构(图像来自 Nicolai Parlog):

图 1. JUnit 5 的架构
JUnit 5 架构示意图。

若是仔细查看图 1,就会发现 JUnit 5 的架构有多么强大。好了,让咱们仔细看看这个架构。右上角的方框代表,对 JUnit 5 而言,JUnit Jupiter API 只是另外一个 API!由于 JUnit Jupiter 的组件遵循新的架构,因此它们可应用 JUnit 5,但您能够轻松定义不一样的测试框架。只要一个框架实现了 TestEngine 接口,就能够将它插入任何支持 junit-platform-engine 和 junit-platform-launcher API 的工具中!

我仍然认为 JUnit Jupiter 很是特殊(毕竟我即将用一整篇教程来介绍它),但 JUnit 5 团队完成的工做确实具备开创性。我只是想指出这一点。咱们继续看看图 1,直到咱们彻底达成一致。

使用 JUnit Jupiter 编写测试内容

就测试编写者而言,任何符合 JUnit 规范的测试框架(包括 JUnit Jupiter)都包含两个组件:

  • 咱们为其编写测试的 API。
  • 理解这个特定 API 的 JUnit TestEngine 实现。

对于本教程,前者是 JUnit Jupiter API,后者是 JUnit Jupiter Test Engine。我将介绍这两者。

JUnit Jupiter API

做为开发人员,您将使用 JUnit Jupiter API 建立单元测试来测试您的应用程序代码。使用该 API 的基本特性 — 注解、断言等 — 是本部分教程的主要关注点。

JUnit Jupiter API 的设计让您可经过插入各类生命周期回调来扩展它的功能。您将在第 2 部分中了解如何使用这些回调完成有趣的工做,好比运行参数化测试,将参数传递给测试方法,等等。

JUnit Jupiter Test Engine

您将使用 JUnit Jupiter Test Engine 发现和执行 JUnit Jupiter 单元测试。该测试引擎实现了 JUnit Platform 中包含的 TestEngine 接口。可将 TestEngine 看做单元测试与用于启动它们的工具(好比 IDE)之间的桥梁。

使用 JUnit Platform 运行测试

在 JUnit 术语中,运行单元测试的过程分为两部分:

  1. 发现测试和建立测试计划
  2. 启动测试计划,以 (1) 执行测试和 (2) 向用户报告结果。

用于发现测试的 API

用于发现测试和建立测试计划的 API 包含在 JUnit Platform 中,由一个 TestEngine 实现。该测试框架将测试发现功能封装到其 TestEngine 实现中。JUnit Platform 负责使用 IDE 和构建工具(好比 Gradle 和 Maven)发起测试发现流程。

测试发现的目的是建立测试计划,该计划中包含一个测试规范。测试规范包含如下组件:

  • 选择器,好比:
    • 要扫描哪一个包来寻找测试类
    • 特定的类名称
    • 特定的方法
    • 类路径根文件夹
  • 过滤器,好比:
    • 类名称模式(好比 “.*Test”)
    • 标签(将在第 2 部分中讨论)
    • 特定的测试引擎(好比 “junit-jupiter”)

测试计划是根据测试规范所发现的全部测试类、这些类中的测试方法、测试引擎等的分层视图。测试计划准备就绪后,就能够执行了。

用于执行测试的 API

用于执行测试的 API 包含在 JUnit Platform 中,由一个或多个 TestEngine 实现。测试框架将测试执行功能封装在它们的 TestEngine 实现中,但 JUnit Platform 负责发起测试执行流程。经过 IDE 和构建工具(好比 Gradle 和 Maven)发起测试执行工做。

一个名为 Launcher 的 JUnit Platform 组件负责执行在测试发现期间建立的测试计划。某个流程 — 假设是您的 IDE — 经过 JUnit Platform(具体来说是 junit-platform-launcher API)发起测试执行流程。这时,JUnit Platform 将测试计划连同 TestExecutionListener 一块儿传递给 LauncherTestExecutionListener 将报告测试执行结果,从而在您的 IDE 中显示该结果。

测试执行流程的目的是向用户准确报告在测试运行时发生了哪些事件。这包括测试成功和失败报告,以及伴随失败而生成的消息,帮助用户理解所发生的事件。

后向兼容性:JUnit Vintage

许多组织对 JUnit 3 和 4 进行了大力投资,所以没法承担向 JUnit 5 的大规模转换。了解到这一点后,JUnit 5 团队提供了junit-vintage-engine 和 junit-jupiter-migration-support 组件来帮助企业进行迁移。

对 JUnit Platform 而言,JUnit Vintage 只是另外一个测试框架,包含本身的 TestEngine 和 API(具体来说是 JUnit 4 API)。

图 2 显示了各类 JUnit 5 包之间的依赖关系。

图 2. JUnit 5 包关系图
JUnit 5 包示意图。

opentest4j 的用途

支持 JUnit 的测试框架在如何处理测试执行期间抛出的异常方面有所不一样。JVM 上的测试没有统一标准,这是 JUnit 团队一直要面对的问题。除了 java.lang.AssertionError,测试框架还必须定义本身的异常分层结构,或者将自身与 JUnit 支持的异常结合起来(或者在某些状况下同时采起两种方法)。

为了解决一致性问题,JUnit 团队提议创建一个开源项目,该项目目前称为 Open Test Alliance for the JVM(JVM 开放测试联盟)。该联盟在此阶段仅是一个提案,它仅定义了初步的异常分层结构。可是,JUnit 5 使用 opentest4j 异常。(可在图 2 中看到这一点;请注意从 junit-jupiter-api 和 junit-platform-engine 包到 opentest4j 包的依赖线。)

如今您已基本了解各类 JUnit 5 组件如何结合在一块儿,是时候使用 JUnit Jupiter API 编写一些测试了!

使用 JUnit Jupiter 编写测试

注解

从 JUnit 4 开始,注解 (annotation) 就成为测试框架的核心特性,这一趋势在 JUnit 5 中得以延续。我没法介绍 JUnit 5 的全部注解,本节仅简要介绍最经常使用的注解。

首先,我将比较 JUnit 4 中与 JUnit 5 中的注解。JUnit 5 团队更改了一些注解的名称,让它们更直观,同时保持功能不变。若是您正在使用 JUnit 4,下表将帮助您适应这些更改。

表 1. JUnit 4 与 JUnit 5 中的注解比较

使用注解

接下来看看一些使用这些注解的示例。尽管一些注解已在 JUnit 5 中重命名,但若是您使用过 JUnit 4,应熟悉它们的功能。清单 1 中的代码来自 JUnit5AppTest.java,可在 HelloJUnit5 示例应用程序中找到。

清单 1. 基本注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RunWith(JUnitPlatform.class)
@DisplayName("Testing using JUnit 5")
public class JUnit5AppTest {
  
   private static final Logger log = LoggerFactory.getLogger(JUnit5AppTest.class);
  
   private App classUnderTest;
  
   @BeforeAll
   public static void init() {
     // Do something before ANY test is run in this class
   }
  
   @AfterAll
   public static void done() {
     // Do something after ALL tests in this class are run
   }
  
   @BeforeEach
   public void setUp() throws Exception {
     classUnderTest = new App();
   }
  
   @AfterEach
   public void tearDown() throws Exception {
     classUnderTest = null;
   }
  
   @Test
   @DisplayName("Dummy test")
   void aTest() {
     log.info("As written, this test will always pass!");
     assertEquals(4, (2 + 2));
   }
  
   @Test
   @Disabled
   @DisplayName("A disabled test")
   void testNotRun() {
     log.info("This test will not run (it is disabled, silly).");
   }
.
.
}

看看上面突出显示行中的注解:

  • 第 1 行:@RunWith 连同它的参数 JUnitPlatform.class(一个基于 JUnit 4 且理解 JUnit Platform 的 Runner)让您能够在 Eclipse 内运行 JUnit Jupiter 单元测试。Eclipse 还没有原生支持 JUnit 5。将来,Eclipse 将提供原生的 JUnit 5 支持,那时咱们再也不须要此注解。
  • 第 2 行:@DisplayName 告诉 JUnit 在报告测试结果时显示 String “Testing using JUnit 5”,而不是测试类的名称。
  • 第 9 行:@BeforeAll 告诉 JUnit 在运行这个类中的全部 @Test 方法以前运行 init() 方法一次
  • 第 14 行:@AfterAll 告诉 JUnit 在运行这个类中的全部 @Test 方法以后运行 done() 方法一次
  • 第 19 行:@BeforeEach 告诉 JUnit 在此类中的每一个@Test 方法以前运行 setUp() 方法。
  • 第 24 行:@AfterEach 告诉 JUnit 在此类中的每一个@Test 方法以后运行 tearDown() 方法。
  • 第 29 行:@Test 告诉 JUnit,aTest() 方法是一个 JUnit Jupiter 测试方法。
  • 第 37 行:@Disabled 告诉 JUnit 不运行此 @Test 方法,由于它已被禁用。

断言

断言 (assertion) 是 org.junit.jupiter.api.Assertions 类上的众多静态方法之一。断言用于测试一个条件,该条件必须计算为 true,测试才能继续执行。

若是断言失败,测试会在断言所在的代码行上中止,并生成断言失败报告。若是断言成功,测试会继续执行下一行代码。

表 2 中列出的全部 JUnit Jupiter 断言方法都接受一个可选的 message 参数(做为最后一个参数),以显示断言是否失败,而不是显示标准的缺省消息。

表 2. JUnit Jupiter 中的断言

清单 2 给出了一个使用这些断言的示例,该示例来自 HelloJUnit5 示例应用程序。

清单 2. 示例应用程序中的 JUnit Jupiter 断言
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
.
.
   @Test
   @DisplayName("Dummy test")
   void dummyTest() {
     int expected = 4;
     int actual = 2 + 2;
     assertEquals(expected, actual, "INCONCEIVABLE!");
     //
     Object nullValue = null;
     assertFalse(nullValue != null);
     assertNull(nullValue);
     assertNotNull("A String", "INCONCEIVABLE!");
     assertTrue(nullValue == null);
     .
     .
   }

看看上面突出显示行中的断言:

  • 第 13 行:assertEquals:若是第一个参数值 (4) 不等于第二个参数值 (2+2),则断言失败。在报告断言失败时使用用户提供的消息(该方法的第 3 个参数)。
  • 第 16 行:assertFalse:表达式 nullValue != null 必须为 false,不然断言失败。
  • 第 17 行:assertNullnullValue 参数必须为 null,不然断言失败。
  • 第 18 行:assertNotNullString 文字值 “A String” 不得为 null,不然断言失败并报告消息 “INCONCEIVABLE!”(而不是缺省的 “Assertion failed” 消息)。
  • 第 19 行:assertTrue:若是表达式 nullValue == null 不等于 true,则断言失败。

除了支持这些标准断言,JUnit Jupiter AP 还提供了多个新断言。下面介绍其中的两个。

方法 @assertAll()

清单 3 中的 @assertAll() 方法给出了清单 2 中看到的相同断言,但包装在一个新的断言方法中:

清单 3. assertAll()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import static org.junit.jupiter.api.Assertions.assertAll;
.
.
@Test
@DisplayName("Dummy test")
void dummyTest() {
   int expected = 4;
   int actual = 2 + 2;
   Object nullValue = null;
   .
   .
   assertAll(
       "Assert All of these",
       () -> assertEquals(expected, actual, "INCONCEIVABLE!"),
       () -> assertFalse(nullValue != null),
       () -> assertNull(nullValue),
       () -> assertNotNull("A String", "INCONCEIVABLE!"),
       () -> assertTrue(nullValue == null));
}

assertAll() 的有趣之处在于,它包含的全部断言都会执行,即便一个或多个断言失败也是如此。与此相反,在清单 2 中的代码中,若是任何断言失败,测试就会在该位置失败,意味着不会执行任何其余断言。

方法 @assertThrows()

在某些条件下,接受测试的类应抛出异常。JUnit 4 经过 expected = 方法参数或一个 @Rule 提供此能力。与此相反,JUnit Jupiter 经过 Assertions 类提供此能力,使它与其余断言更加一致。

咱们将所预期的异常视为能够进行断言的另外一个条件,所以 Assertions 包含处理此条件的方法。清单 4 引入了新的assertThrows() 断言方法。

清单 4. assertThrows()
1
2
3
4
5
6
7
8
9
10
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
.
.
@Test()
@DisplayName("Empty argument")
public void testAdd_ZeroOperands_EmptyArgument() {
   long[] numbersToSum = {};
   assertThrows(IllegalArgumentException.class, () -> classUnderTest.add(numbersToSum));
}

请注意第 9 行:若是对 classUnderTest.add() 的调用没有抛出 IllegalArgumentException,则断言失败。

前置条件

前置条件 (Assumption) 与断言相似,但前置条件必须为 true,不然测试将停止。与此相反,当断言失败时,则将测试视为已失败。测试方法只应在某些条件 —前置条件下执行时,前置条件颇有用。

前置条件是 org.junit.jupiter.api.Assumptions 类的静态方法。要理解前置条件的价值,只需一个简单的示例。

假如您只想在星期五运行一个特定的单元测试(我假设您有本身的理由):

1
2
3
4
5
6
7
@Test
@DisplayName("This test is only run on Fridays")
public void testAdd_OnlyOnFriday() {
   LocalDateTime ldt = LocalDateTime.now();
   assumeTrue(ldt.getDayOfWeek().getValue() == 5);
   // Remainder of test (only executed if assumption holds)...
}

在此状况下,若是条件不成立(第 5 行),就不会执行 lambda 表达式的内容。

请注意第 5 行:若是该条件不成立,则跳过该测试。在此状况下,该测试不是在星期五 (5) 运行的。这不会影响项目的 “绿色” 部分,并且不会致使构建失败;会跳过 assumeTrue() 后的测试方法中的全部代码。

若是在前置条件成立时仅应执行测试方法的一部分,可使用 assumingThat() 方法编写上述条件,该方法使用 lambda 语法:

1
2
3
4
5
6
7
8
9
10
@Test
@DisplayName("This test is only run on Fridays (with lambda)")
public void testAdd_OnlyOnFriday_WithLambda() {
   LocalDateTime ldt = LocalDateTime.now();
   assumingThat(ldt.getDayOfWeek().getValue() == 5,
       () -> {
         // Execute this if assumption holds...
       });
   // Execute this regardless
}

注意,不管 assumingThat() 中的前置条件成立与否,都会执行 lambda 表达式后的全部代码。

嵌套单元测试,实现清晰的结构

在继续介绍下节内容以前,我想介绍在 JUnit 5 中编写单元测试的最后一个特性。

JUnit Jupiter API 容许您建立嵌套的类,以保持测试代码更清晰,这有助于让测试结果更易读。经过在主类中建立嵌套的测试类,能够建立更多的名称空间,这提供了两个主要优点:

  • 每一个单元测试能够拥有本身的测试前和测试后生命周期。这让您能使用特殊条件建立要测试的类,从而测试极端状况。
  • 单元测试方法的名称变得更简单。在 JUnit 4 中,全部测试方法都以对等形式存在,不容许重复的方法名(因此您最终会获得相似 testMethodButOnlyUnderThisOrThatCondition_2() 的方法名)。从 JUnit Jupiter 开始,只有嵌套类中的方法必须具备惟一的名称。清单 6 展现了这一优点。
清单 5. 传递一个空或 null 数组引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(JUnitPlatform.class)
@DisplayName("Testing JUnit 5")
public class JUnit5AppTest {
.
.               
   @Nested
   @DisplayName("When zero operands")
   class JUnit5AppZeroOperandsTest {
  
   // @Test methods go here...
  
   }
.
.
}

请注意第 6 行,其中的 JUnit5AppZeroOperandsTest 类能够拥有测试方法。任何测试的结果都会在父类 JUnit5AppTest 中以嵌套的形式显示。

使用 JUnit Platform 运行测试

能编写单元测试很不错,但若是不能运行它们,就没有什么意义了。本节展现如何在 Eclipse 中运行 JUnit 测试,首先使用 Maven,而后从命令行使用 Gradle。

下面的视频展现了如何从 GitHub 克隆示例应用程序代码,并在 Eclipse 中运行测试。在该视频中,我还展现了如何从命令行以及 Eclipse 内使用 Maven 和 Gradle 运行单元测试。Eclipse 对 Maven 和 Gradle 都提供了很好的支持。

应用 3 种工具运行单元测试

应用 3种工具运行单元测试

点击查看视频演示查看抄本

下面将提供一些简要的说明,但该视频提供了更多细节。观看该视频,了解如何:

  • 从 GitHub 克隆 HelloJUnit5 示例应用程序。
  • 将应用程序导入 Eclipse 中。
  • 从 Eclipse 内的 HelloJUnit5 应用程序运行一个 JUnit 测试。
  • 使用 Maven 从命令行运行 HelloJUnit5 单元测试。
  • 使用 Gradle 从命令行运行 HelloJUnit5 单元测试。

克隆 HelloJUnit5 示例应用程序

要理解教程的剩余部分,您须要从 GitHub 克隆示例应用程序。为此,可打开一个终端窗口 (Mac) 或命令提示 (Windows),导航到您但愿放入代码的目录,而后输入如下命令:

git clone https://github.com/makotogo/HelloJUnit5

如今您的机器上已拥有该代码,能够在 Eclipse IDE 内运行 JUnit 测试了。接下来介绍如何运行测试。

在 Eclipse IDE 中运行单元测试

若是您已跟随该视频进行操做,应该已将代码导入 Eclipse 中。如今,在 Eclipse 中打开 Project Explorer 视图,展开 HelloJUnit5 项目,直至看到 src/test/java 路径下的 JUnit5AppTest 类。

打开 JUnit5AppTest.java 并验证 class 定义前的下面这个注解(如下代码的第 3 行):

1
2
3
4
5
6
7
.
.
@RunWith(JUnitPlatform.class)
public class JUnit5AppTest {
.
.
}

如今右键单击 JUnit5AppTest 并选择 Run As > JUnit Test。单元测试运行时,JUnit 视图将会出现。您如今已准备好完成本教程的练习。

使用 Maven 运行单元测试

打开一个终端窗口 (Mac) 或命令提示 (Windows),导航到您将 HelloJUnit5 应用程序克隆到的目录,而后输入如下命令:

mvn test

这会启动 Maven 构建并运行单元测试。您的输出应相似于:

$ mvn test
[INFO] Scanning for projects...
[INFO]                                                                        
[INFO] ------------------------------------------------------------------------
[INFO] Building HelloJUnit5 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ HelloJUnit5 ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/sperry/home/projects/learn/HelloJUnit5/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.6.1:compile (default-compile) @ HelloJUnit5 ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ HelloJUnit5 ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/sperry/home/projects/learn/HelloJUnit5/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.6.1:testCompile (default-testCompile) @ HelloJUnit5 ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.19:test (default-test) @ HelloJUnit5 ---
  
-------------------------------------------------------
  T E S T S
-------------------------------------------------------
Running com.makotojava.learn.hellojunit5.JUnit5AppTest
17:08:56.137 [main] INFO com.makotojava.learn.hellojunit5.JUnit5AppTest - As written, this test will always pass!
Tests run: 2, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.112 sec - in com.makotojava.learn.hellojunit5.JUnit5AppTest
Running com.makotojava.learn.hellojunit5.solution.JUnit5AppTest
17:08:56.166 [main] INFO com.makotojava.learn.hellojunit5.solution.JUnit5AppTest - As written, this test will always pass!
Tests run: 11, Failures: 0, Errors: 0, Skipped: 2, Time elapsed: 0.052 sec - in com.makotojava.learn.hellojunit5.solution.JUnit5AppTest
  
Results :
  
Tests run: 13, Failures: 0, Errors: 0, Skipped: 3
  
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.250 s
[INFO] Finished at: 2017-04-29T17:08:56-05:00
[INFO] Final Memory: 11M/309M
[INFO] ------------------------------------------------------------------------

使用 Gradle 运行单元测试

打开一个终端窗口 (Mac) 或命令提示 (Windows),导航到您将 HelloJUnit5 应用程序克隆到的目录,而后输入此命令:

gradle clean test

输出应相似于:

$ gradle clean test
:clean
:compileJava
:processResources NO-SOURCE
:classes
:compileTestJava
:processTestResources NO-SOURCE
:testClasses
:junitPlatformTest
Download https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.6.2/log4j-jul-2.6.2.pom
Download https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.6.2/log4j-jul-2.6.2.jar
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
19:44:36.657 [main] INFO com.makotojava.learn.hellojunit5.JUnit5AppTest - As written, this test will always pass!
19:44:36.667 [main] INFO com.makotojava.learn.hellojunit5.solution.JUnit5AppTest - As written, this test will always pass!
  
Test run finished after 10145 ms
[         8 containers found      ]
[         0 containers skipped    ]
[         8 containers started    ]
[         0 containers aborted    ]
[         8 containers successful ]
[         0 containers failed     ]
[        13 tests found           ]
[         2 tests skipped         ]
[        11 tests started         ]
[         1 tests aborted         ]
[        10 tests successful      ]
[         0 tests failed          ]
  
:test SKIPPED
  
BUILD SUCCESSFUL
  
Total time: 18.301 secs

测试练习

如今您已了解 JUnit Jupiter,查看了代码示例,并观看了视频(但愿您已跟随视频进行操做)。很是棒,但没有什么比动手编写代码更有用了!在第 1 部分的最后一节,您将完成如下任务:

  • 编写 JUnit Jupiter API 单元测试。
  • 运行单元测试。
  • 实现 App 类,让您的单元测试经过检查。

采用真正的测试驱动开发 (TDD) 方式,首先编写单元测试,运行它们,并会观察到它们所有失败了。而后编写实现,直到单元测试经过,这时您就大功告成了。

注意,JUnit5AppTest 类仅提供了两个现成的测试方法。首次运行该类时,两者都是 “绿色” 的。要完成这些练习,您须要添加剩余的代码,包括用于告诉 JUnit 运行哪些测试方法的注解。记住,若是没有正确配备一个类或方法,JUnit 将跳过它。

若是遇到困难,请查阅 com.makotojava.learn.hellojunit5.solution 包来寻找解决方案。

1

编写 JUnit Jupiter 单元测试

首先从 JUnit5AppTest.java 开始。打开此文件并按照 Javadoc 注解中的指示操做。

提示:使用 Eclipse 中的 Javadoc 视图读取测试指令。要打开 Javadoc 视图,能够转到 Window > Show View > Javadoc。您应该看到 Javadoc 视图。根据您设置工做区的方式,该窗口可能出如今任意多个位置。在个人工做区中,该窗口与图 3 中的屏幕截图相似,出如今 IDE 右侧的编辑器窗口下方:

图 3. Javadoc 视图
Javadoc 视图的屏幕截图。

编辑器窗口中显示了具备原始 HTML 标记的 Javadoc 注解,但在 Javadoc 窗口中,已将其格式化,所以更易于阅读。

2

在 Eclipse 中运行单元测试

若是您像我同样,您会使用 IDE 执行如下工做:

  • 编写单元测试。
  • 编写单元测试所测试的实现内容。
  • 运行初始测试(使用 IDE 的原生 JUnit 支持)。

JUnit 5 提供了一个名为 JUnitPlatform 的类,它容许您在 Eclipse 中运行 JUnit 5 测试。

要在 Eclipse 中运行测试,须要确保您的计算机上拥有示例应用程序。为此,最轻松的方法是从 GitHub 克隆 HelloJUnit5 应用程序,而后将它导入 Eclipse 中。(由于本教程的视频展现了如何这么作,因此这里将跳过细节,仅提供操做步骤。)

确保您克隆了 GitHub 存储库,而后将代码导入 Eclipse 中做为新的 Maven 项目。

将该项目导入 Eclipse 中后,打开 Project Explorer 视图并展开 src/main/test 节点,直至看到 JUnit5AppTest。要以 JUnit 测试的形式运行它,能够右键单击它,选择 Run As > JUnit Test

3

实现 App 类,直到单元测试经过检查

App 的单一 add() 方法提供的功能很容易理解,并且在设计上很是简单。我不但愿复杂应用程序的业务逻辑阻碍您对 JUnit Jupiter 的学习。

单元测试经过后,您就大功告成了!记住,若是遇到困难,能够在 com.makotojava.learn.hellojunit5.solution 包中查找解决方案。

第 1 部分小结

在 JUnit 5 教程的前半部分中,我介绍了 JUnit 5 的架构和组件,并详细介绍了 JUnit Jupiter API。咱们逐个介绍了 JUnit 5 中最经常使用的注解、断言和前置条件,并且经过一个快速练习演示了如何在 Eclipse、Maven 和 Gradle 中运行测试。

第 2 部分中,您将了解 JUnit 5 的一些高级特性:

  • JUnit Jupiter 扩展模型
  • 方法参数注入
  • 参数化测试

那么您接下来会怎么作?

第 2 部分

JUnit 5 Vintage 和 JUnit Jupiter 扩展模型

了解用于参数注入、参数化测试、动态测试和自定义注解的 JUnit Jupiter 扩展

在本教程的第 1 部分中,我介绍了 JUnit 5 的设置说明,以及 JUnit 5 的架构和组件。还介绍了如何使用 JUnit Jupiter API 中的新特性,包括注解、断言和前置条件。

在本部分中,您将熟悉组成全新 JUnit 5 的另外两个模块:JUnit Vintage 和 JUnit Jupiter 扩展模型。我将介绍如何使用这些组件实现参数注入、参数化测试、动态测试和自定义注解等。

与第 1 部分中同样,我将介绍如何使用 Maven 和 Gradle 运行测试。

请注意,本教程的示例基于 JUnit 5, Milestone 5

前提条件

假设您熟悉如下软件的使用:

  • Eclipse IDE
  • Maven
  • Gradle(可选)
  • Git

要跟随示例进行操做,您应在计算机上安装 JDK 八、Eclipse、Maven、Gradle(可选)和 Git。若是缺乏其中的任何工具,可以使用下面的连接下载和安装它们:

JUnit Vintage

升级到新的重要软件版本始终存在风险,可是在这里,升级不只是个好主意,并且还很安全。

由于许多组织对 JUnit 4 (甚至对 JUnit 3)进行了大力投资,因此 JUnit 5 的开发团队建立了 JUnit Vintage 包,其中包含 JUnit Vintage 测试引擎。JUnit Vintage 可确保现有 JUnit 测试能与使用 JUnit Jupiter 建立的新测试一同运行。

JUnit 5 的架构还支持同时运行多个测试引擎:能够一同运行 JUnit Vintage 测试引擎和任何其余兼容 JUnit 5 的测试引擎。

如今您已了解 JUnit Vintage,可能想知道它的工做原理。图 1 给出了来自第 1 部分的 JUnit 5 依赖关系图,展现了 JUnit 5 中各类包之间的关系。

图 1. JUnit 5 依赖关系图
JUnit 5 依赖关系示意图。

图 1 中间行中所示的 JUnit Vintage 旨在提供一条通往 JUnit Jupiter 的 “平稳升级路径”。两个 JUnit 5 模块依赖于 JUnit Vintage:

  • junit-platform-runner 提供一个 Runner,容许在 JUnit 4 环境(好比 Eclipse)中执行测试。
  • junit-jupiter-migration-support 提供了后向兼容性,容许您选择 JUnit 4 Rule

JUnit Vintage 自己由两个模块组成:

  • junit:junit 是用于 JUnit 3 和 JUnit 4 的 API。
  • junit-vintage-engine 是在 JUnit Platform 上运行 JUnit 3 和 JUnit 4 测试的测试引擎。

由于 JUnit Platform 容许多个测试引擎同时运行,因此可以让您的 JUnit 3 和 JUnit 4 测试与使用 JUnit Jupiter 编写的测试并列运行。教程后面将介绍如何执行该操做。

在 Eclipse、Maven 和 Gradle 中运行测试以前,咱们花点时间复习一下基本单元测试的概念。咱们将分析在 JUnit 3 和 JUnit 4 中编写的测试。

JUnit 3 中的测试

使用 JUnit 3 编写的测试将按原样在 JUnit Platform 上运行。只需将 junit-vintage 依赖项包含在构建版本中,其余部分就能直接运行。

在示例应用程序中,您将看到已包含在示例应用程序中的 Maven POM (pom.xml) 和 Gradle 构建文件 (build.gradle),因此您可当即运行这些测试。

清单 1 给出了示例应用程序的一个 JUnit 3 测试的部分内容。它位于 com.makotojava.learn.junit3 包中的 src/test/java树中。

清单 1. HelloJunit5Part2 示例应用程序的 JUnit 3 测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
.
public class PersonDaoBeanTest extends TestCase {
 
   private ApplicationContext ctx;
 
   private PersonDaoBean classUnderTest;
 
   @Override
   protected void setUp() throws Exception {
     ctx = new AnnotationConfigApplicationContext(TestSpringConfiguration.class);
     classUnderTest = ctx.getBean(PersonDaoBean.class);
   }
 
   @Override
   protected void tearDown() throws Exception {
     DataSource dataSource = (DataSource) ctx.getBean("dataSource");
     if (dataSource instanceof EmbeddedDatabase) {
       ((EmbeddedDatabase) dataSource).shutdown();
     }
   }
 
   public void testFindAll() {
     assertNotNull(classUnderTest);
     List< Person > people = classUnderTest.findAll();
     assertNotNull(people);
     assertFalse(people.isEmpty());
     assertEquals(5, people.size());
   }
.
.
}

JUnit 3 测试用例扩展了 JUnit 3 API 类 TestCase(第 3 行),每一个测试方法必须以单词 test 开头(第 23 行)。

要在 Eclipse 中运行此测试,可右键单击 Package Explorer 视图中的测试类,选择 Run As > Junit Test

教程后面将介绍如何使用 Maven 和 Gradle 运行此测试。

JUnit 4 中的测试

您的 JUnit 4 测试按原样在 JUnit Platform 上运行。只需将 junit-vintage 依赖项包含在构建版本中,就能直接运行它。

示例应用程序中包含的 Maven POM 和 Gradle 构建文件 (build.gradle) 中已包含该依赖项,因此您可当即运行这些测试。

清单 2 给出了示例应用程序的一个 JUnit 4 测试的部分内容。它位于 com.makotojava.learn.junit4 包中的 src/test/java树中。

清单 2. HelloJunit5Part2 示例应用程序的 JUnit 4 测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.
.
public class PersonDaoBeanTest {
 
   private ApplicationContext ctx;
 
   private PersonDaoBean classUnderTest;
 
   @Before
   public void setUp() throws Exception {
     ctx = new AnnotationConfigApplicationContext(TestSpringConfiguration.class);
     classUnderTest = ctx.getBean(PersonDaoBean.class);
   }
 
   @After
   public void tearDown() throws Exception {
     DataSource dataSource = (DataSource) ctx.getBean("dataSource");
     if (dataSource instanceof EmbeddedDatabase) {
       ((EmbeddedDatabase) dataSource).shutdown();
     }
   }
 
   @Test
   public void findAll() {
     assertNotNull(classUnderTest);
     List< Person > people = classUnderTest.findAll();
     assertNotNull(people);
     assertFalse(people.isEmpty());
     assertEquals(5, people.size());
   }
.
.
}

JUnit 4 测试用例以单词 Test 结尾(第 3 行),每一个测试方法使用 @Test 注解(第 23 行)。

要在 Eclipse 中运行此测试,可右键单击 Package Explorer 视图中的测试类,选择 Run As > Junit Test

教程后面将介绍如何使用 Maven 和 Gradle 运行此测试。

对迁移到 JUnit Jupiter 的支持

junit-jupiter-migration-support 包中包含了用于后向兼容性的一些选定 Rule,因此若是您对 JUnit 4 规则进行了大力投资也不用担忧。在 JUnit 5 中,您将使用 JUnit Jupiter 扩展模型实现 JUnit 4 中的各类规则提供的相同行为。下一节将介绍如何完成该工做。

JUnit Jupiter 扩展模型

经过使用 JUnit 扩展模型,如今任何开发人员或工具供应商都能扩展 JUnit 的核心功能。

要想真正认识到 JUnit Jupiter 扩展模型的开创性,须要理解它如何扩展 JUnit 4 的核心功能。若是您已理解这一点,可跳过下一节。

扩展 JUnit 4 的核心功能

过去,但愿扩展 JUnit 4 核心功能的开发人员或工具供应商会使用 Runner 和 @Rule

Runner 一般是 BlockJUnit4ClassRunner 的子类,用于提供 JUnit 中没有直接提供的某种行为。目前有许多第三方 Runner,好比用于运行基于 Spring 的单元测试的 SpringJUnit4ClassRunner,以及用于处理单元测试中 Mockito 对象的MockitoJUnitRunner

必须在测试类级别上使用 @RunWith 注解来声明 Runner@RunWith 接受一个参数:Runner 的实现类。由于每一个测试类最多只能拥有一个 Runner,因此每一个测试类最多也只能拥有一个扩展点。

为了解决 Runner 概念的这一内置限制,JUnit 4.7 引入了 @Rule。一个测试类可声明多个 @Rule,这些规则可在测试方法级别和类级别上运行(而 Runner 只能在类级别上运行)。

鉴于 JUnit 4.7 的 @Rule 解决方法很好地处理了大部分状况,您可能想知道为何咱们还须要新的 JUnit Jupiter 扩展模型。下节将解释其中的缘由。

特性与扩展

JUnit 5 的一个核心原则是扩展点优于特性

这意味着尽管 JUnit 为工具供应商和开发人员提供各类特性,但 JUnit 5 团队更喜欢在架构中提供扩展点。这样第三方(不管是工具供应商、测试编写者仍是其余任何人)就能在这些点上编写各类扩展。根据 JUnit Wiki 的解释,优先选择扩展点有 3 个缘由:

  • JUnit 不是,也不会尝试成为一个无所不包的实用程序。
  • 第三方开发人员知道他们的需求,而且编写代码来知足本身需求的速度比 JUnit 团队响应某个特性请求的速度更快。
  • API 一旦发布,就很难更改

接下来我将解释如何扩展 JUnit Jupiter API,首先从扩展点开始。

扩展点和测试生命周期

一个扩展点对应于 JUnit test 生命周期中一个预约义的点。从 Java™ 语言的角度讲,扩展点是您实现并向 JUnit 注册(激活)的回调接口。所以,扩展点是回调接口,扩展是该接口的实现。

在本教程中,我将把已实现的扩展点回调接口称为扩展

一旦注册您的扩展,就会将其激活。在测试生命周期中合适的点上,JUnit 将使用回调接口调用它。

表 1 总结了 JUnit Jupiter 扩展模型中的扩展点。

表 1. 扩展点

表 1 中列出的扩展点回调接口已在示例应用程序的 JUnit5ExtensionShowcase 类中实现。可在com.makotojava.learn.junit5 包中的 test/src 树中找到该类。

建立扩展

要建立扩展,只需实现该扩展点的回调接口。假设我想建立一个在每一个测试方法运行以前就运行的扩展。在此状况下,我只须要实现 BeforeEachCallback 接口:

1
2
3
4
5
6
public class MyBeforeEachCallbackExtension implements BeforeEachCallback {
   @Override
   public void beforeEach(ExtensionContext context) throws Exception {
     // Implementation goes here
   }
}

实现扩展点接口后,须要激活它,这样 JUnit 才能在测试生命周期中合适的点调用它。经过注册扩展来激活它。

激活扩展

要激活上述扩展,只需使用 @ExtendWith 注解注册它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ExtendWith(MyBeforeEachCallbackExtension.class)
public class MyTestClass {
.
.
     @Test
     public void myTestMethod() {
         // Test code here
     }
     @Test
     public void someOtherTestMethod() {
         // Test code here
     }
.
.
}

当 MyTestClass 运行时,在执行每一个 @Test 方法前,会调用 MyBeforeEachCallbackExtension

注意,这种注册扩展的风格是声明性的。JUnit 还提供了一种自动注册机制,它使用了 Java 的 ServiceLoader 机制。此处不会详细介绍该机制,但 JUnit 5 用户指南的扩展模型部分中提供了大量的有用信息。

参数注入

假设您想将一个参数传递给 @Test 方法。您如何完成该工做?下面咱们就学习一下。

ParameterResolver 接口

若是所编写的测试方法在其签名中包含一个参数,则必须将该参数解析为一个实际对象,而后 JUnit 才能调用该方法。一种乐观的场景以下所示:JUnit (1) 寻找一个实现 ParameterResolver 接口的已注册扩展;(2) 调用它来解析该参数;(3) 而后调用您的测试方法,传入解析后的参数值。

ParameterResolver 接口包含 2 个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.junit.jupiter.api.extension;
 
import static org.junit.platform.commons.meta.API.Usage.Experimental;
import java.lang.reflect.Parameter;
import org.junit.platform.commons.meta.API;
 
@API(Experimental)
public interface ParameterResolver extends Extension {
 
     boolean supportsParameter(ParameterContext parameterContext,
                               ExtensionContext extensionContext)
             throws ParameterResolutionException;
 
     Object resolveParameter(ParameterContext parameterContext,
                             ExtensionContext extensionContext)
             throws ParameterResolutionException;
 
}

Jupiter 测试引擎须要解析您的测试类中的一个参数时,它首先会调用 supports() 方法,查看该扩展是否能处理这种参数类型。若是 supports() 返回 true,则 Jupiter 测试引擎调用 resolve() 来获取正确类型的 Object,随后在调用测试方法时会使用该对象。

若是未找到能处理该参数类型的扩展,您会看到一条与下面相似的消息:

1
2
3
4
5
org.junit.jupiter.api.extension.ParameterResolutionException:
No ParameterResolver registered for parameter [java.lang.String arg0] in executable
[public void com.makotojava.learn.junit5.PersonDaoBeanTest$WhenDatabaseIsPopulated.findAllByLastName(java.lang.String)].
.
.

建立 ParameterResolver 实现

要建立一个 ParameterResolver,您只需实现该接口:

清单 3. Person 对象的 ParameterResolver 扩展点实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
 
import com.makotojava.learn.junit.Person;
import com.makotojava.learn.junit.PersonGenerator;
 
public class GeneratedPersonParameterResolver implements ParameterResolver {
 
   @Override
   public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
       throws ParameterResolutionException {
     return parameterContext.getParameter().getType() == Person.class;
   }
 
   @Override
   public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
       throws ParameterResolutionException {
     return PersonGenerator.createPerson();
   }
 
}

在这个特定的用例中,若是参数的类型是 Person(第 14 行),则 supports() 返回 true。JUnit 须要将参数解析为 Person对象时,它调用 resolve(),后者返回一个新生成的 Person 对象(第 20 行)。

使用 ParameterResolver 实现

要使用 ParameterResolver,必须向 JUnit Jupiter 测试引擎注册它。与前面的演示同样,可以使用 @ExtendWith 注解完成注册工做。

清单 4. 使用 ParameterResolver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@DisplayName("Testing PersonDaoBean")
@ExtendWith(GeneratedPersonParameterResolver.class)
public class PersonDaoBeanTest extends AbstractBaseTest {
.
.
     @Test
     @DisplayName("Add generated Person should succeed - uses Parameter injection")
     public void add(Person person) {
       assertNotNull(classUnderTest, "PersonDaoBean reference cannot be null.");
       Person personAdded = classUnderTest.add(person);
       assertNotNull(personAdded, "Add failed but should have succeeded");
       assertNotNull(personAdded.getId());
       performPersonAssertions(person.getLastName(), person.getFirstName(), person.getAge(), person.getEyeColor(),
           person.getGender(), personAdded);
     }
.
.
}

PersonDaoBeanTest 类运行时,它将向 Jupiter 测试引擎注册 GeneratedPersonParameterResolver。每次须要解析一个参数时,就会调用自定义 ParameterResolver

扩展有一个影响范围 - 类级别或方法级别。

在这个特定的用例中,我选择在类级别注册扩展(第 2 行)。在类级别注册意味着,接受任何参数的任何测试方法都会致使 JUnit 调用 GeneratedPersonParameterResolver 扩展。若是参数类型为 Person,则返回一个已生成的 Person 对象并将其传递给测试方法(第 8 行)。

要将扩展的范围缩小到单个方法,可按以下方式注册扩展:

清单 5. 仅将 ParameterResolver 用于单个方法
1
2
3
4
5
6
7
8
9
10
11
@Test
@DisplayName("Add generated Person should succeed - uses Parameter injection")
@ExtendWith(GeneratedPersonParameterResolver.class)