JUnit 5 简介

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

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

第 1 部分

JUnit 5 Jupiter API

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

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

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

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

前提条件

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

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

要跟随示例进行操作,您应在计算机上安装 JDK 8、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 8、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)
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);
}

现在,系统只会调用该扩展来解析 add() 测试方法的参数。如果类中的任何其他测试方法需要参数解析,它们需要一个不同的ParameterResolver

注意,任何给定类的特定范围上只能有一个ParameterResolver。举例而言,如果您已经为在类级别上声明的 Person 对象提供了一个 ParameterResolver,并在同一个类中为在方法级别上声明的对象提供了另一个 ParameterResolver,那么 JUnit 就不知道使用哪一个。最终会看到以下消息来表明这种模糊性:

1
2
3
4
5
6
org.junit.jupiter.api.extension.ParameterResolutionException:
Discovered multiple competing ParameterResolvers for parameter
[com.makotojava.learn.junit.Person arg0] in executable
[public void com.makotojava.learn.junit5.PersonDaoBeanTest$WhenDatabaseIsPopulated.update(com.makotojava.learn.junit.Person)]: .
.
.

准备看个视频来放松一下?

下节将介绍参数化测试,但首先让我们用少许时间进行一些实践学习。下面的视频演示了如何在 JUnit 5 中使用ParameterResolver 和 @ParameterizedTest 注解来测试基于 Spring 的应用程序。

JUnit5 的高级特性

JUnit5 的高级特性

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

参数化测试

参数化测试是指多次调用 @Test 方法,但每次都使用不同的参数值。参数化测试必须使用 @ParameterizedTest 进行注解,而且必须为其参数指定一个来源

JUnit Jupiter 提供了多个来源。每个来源指定一个 @ArgumentsSource,也就是一个 ArgumentsProvider 实现。本节将介绍如何使用 3 个来源:

  • @ValueSource
  • @EnumSource
  • @MethodSource

每个来源都在所允许的数据类型的易用性与灵活性之间进行了折中。最容易使用但最不灵活(仅限于一个 Java 原语子集)的是 @ValueSource。最灵活的是 @MethodSource,允许您使用所选的任何复杂对象来参数化测试方法。(注意,@MethodSource 也是最难使用的。)

@ValueSource

在 @ValueSource 中,您指定单个文字值数组,系统将这些文字值 — 一次一个地 — 提供给您的 @ParameterizedTest 方法。

语法类似于:

1
2
3
4
5
6
7
8
@ParameterizedTest
@ValueSource(longs = { 1L, 2L, 3L, 4L, 5L })
public void findById(Long id) {
   assertNotNull(classUnderTest);
   Person personFound = classUnderTest.findById(id);
   assertNotNull(personFound);
   assertEquals(id, personFound.getId());
}

首先您告诉 JUnit,findById() 方法是一个 @ParameterizedTest,如上面第 1 行所示。然后使用数组初始化器语法来指定数组,如第 2 行所示。JUnit 将调用 findById() 测试方法,每次将数组中的下一个 long 传递给该方法(第 3 行),直到用完数组。您可像任何 Java 方法参数一样使用该参数(第 5 行)。

作为数组名所提供的 @ValueSource 属性名必须全部采用小写,而且必须与其末尾有字母 s 的类型相匹配。例如,ints 与 int数组匹配,strings 与 String 数组匹配,等等。

并不支持所有的原语类型,仅支持以下类型:

  • String
  • int
  • long
  • double

@EnumSource

在 @EnumSource 中,您指定一个 enum,JUnit — 一次一个地 — 将其中的值提供给 @ParameterizedTest 方法。

语法类似于:

1
2
3
4
5
6
7
8
9
10
@ParameterizedTest
@EnumSource(PersonTestEnum.class)
public void findById(PersonTestEnum testPerson) {
   assertNotNull(classUnderTest);
   Person person = testPerson.getPerson();
   Person personFound = classUnderTest.findById(person.getId());
   assertNotNull(personFound);
   performPersonAssertions(person.getLastName(), person.getFirstName(), person.getAge(), person.getEyeColor(),
       person.getGender(), personFound);
}

首先您告诉 JUnit,findById() 方法是一个 @ParameterizedTest,如第 1 行所示。然后指定该 enum 的 Java 类,如第 2 行所示。JUnit 将调用 findById() 测试方法,每次将下一个 enum 值传递给该方法(第 3 行),直到用完该 enum。您可像任何 Java 方法参数一样使用该参数(第 5 行)。

注意,PersonTestEnum 类包含在本教程的配套示例应用程序中。它位于 com.makotojava.learn.junit 包中的 src/test/java 树中。

@MethodSource

使用注解 @MethodSource,可以指定您喜欢的任何复杂对象作为测试方法的参数类型。语法类似于:

1
2
3
4
5
6
7
8
9
10
11
@ParameterizedTest
@MethodSource(value = "personProvider")
public void findById(Person paramPerson) {
   assertNotNull(classUnderTest);
   long id = paramPerson.getId();
   Person personFound = classUnderTest.findById(id);
   assertNotNull(personFound);
   performPersonAssertions(paramPerson.getLastName(), paramPerson.getFirstName(),
       paramPerson.getAge(),
       paramPerson.getEyeColor(), paramPerson.getGender(), personFound);
}

@MethodSource 的 names 属性用于指定一个或多个方法名,这些方法为测试方法提供参数。一个方法来源的返回类型必须是StreamIteratorIterable 或数组。此外,提供者方法必须声明为 static,所以不能将它用在 @Nested 测试类内(至少截至 JUnit 5 Milestone 5 时不能这么做)。

在上面的示例中,personProvider 方法(来自示例应用程序)类似于:

1
2
3
4
5
6
7
8
static Iterator< Person > personProvider() {
     PersonTestEnum[] testPeople = PersonTestEnum.values();
     Person[] people = new Person[testPeople.length];
     for (int aa = 0; aa < testPeople.length; aa++) {
       people[aa] = testPeople[aa].getPerson();
     }
     return Arrays.asList(people).iterator();
}

假设您想为测试方法添加一个额外的参数提供者。可以这样声明它:

1
2
3
4
5
6
7
8
9
10
11
@ParameterizedTest
@MethodSource(value = { "personProvider", "additionalPersonProvider" })
public void findById(Person paramPerson) {
   assertNotNull(classUnderTest);
   long id = paramPerson.getId();
   Person personFound = classUnderTest.findById(id);
   assertNotNull(personFound);
   performPersonAssertions(paramPerson.getLastName(), paramPerson.getFirstName(),
       paramPerson.getAge(),
       paramPerson.getEyeColor(), paramPerson.getGender(), personFound);
}

我们使用数组初始化器语法指定这些方法(第 2 行),而且将按您指定的顺序调用各个方法,最后调用的是additionalPersonProvider()

自定义显示名称

参数化测试的缺省显示名称包含测试索引(一个从 1 开始的迭代编号),以及该参数的 String 表示。如果测试类中有多个测试方法,那么输出容易让人混淆。幸运的是,可以通过向 @ParameterizedTest 注解提供任何以下属性值来自定义输出:

  • {index}:从 1 开始的索引(当前测试迭代 )。
  • {arguments}:完整的参数列表,使用逗号分隔。
  • {0}, {1} ...:一个特定的参数(0 是第一个,依此类推)。

举例而言,假设提供了一个包含 5 个 long 的数组。在此情况下,可像这样注解 @ParameterizedTest

1
2
3
4
5
6
7
8
@ParameterizedTest(name = "@ValueSource: FindById(): Test# {index}: Id: {0}")
@ValueSource(longs = { 1L, 2L, 3L, 4L, 5L })
public void findById(Long id) {
   assertNotNull(classUnderTest);
   Person personFound = classUnderTest.findById(id);
   assertNotNull(personFound);
   assertEquals(id, personFound.getId());
}

将会生成以下输出:

@ValueSource: FindById(): Test# 1: Id: 1
@ValueSource: FindById(): Test# 2: Id: 2
@ValueSource: FindById(): Test# 3: Id: 3
@ValueSource: FindById(): Test# 4: Id: 4
@ValueSource: FindById(): Test# 5: Id: 5

动态测试

目前为止,我们分析的都是静态测试,这意味着测试代码、测试数据和测试的通过/失败条件在编译时都是已知的。

JUnit Jupiter 引入了一种称为动态测试的新测试类型,这种测试在运行时由一个称为测试工厂的特殊方法生成。

@TestFactory

@TestFactory 方法用于生成动态测试。此方法必须返回 DynamicTest 实例的 StreamCollectionIterable 或Iterator

不同于 @Test 方法,DynamicTest 实例没有生命周期回调。所以 @BeforeEach@AfterEach 和表 1 中的其他生命周期回调都不适用于 DynamicTest

创建 @TestFactory

考虑来自示例应用程序中 PersonDaoBeanTest 类的以下代码(可在 com.makotojava.learn.junit5 包的 src/test/java 树中找到它):

1
2
3
4
5
6
7
8
9
10
11
12
13
@TestFactory
@DisplayName("FindById - Dynamic Test Generator")
Stream< DynamicTest > generateFindByIdDynamicTests() {
   Long[] ids = { 1L, 2L, 3L, 4L, 5L };
   return Stream.of(ids).map(id -> dynamicTest("DynamicTest: Find by ID " + id, () -> {
     Person person = classUnderTest.findById(id);
     assertNotNull(person);
     int index = id.intValue() - 1;
     Person testPerson = PersonTestEnum.values()[index].getPerson();
     performPersonAssertions(testPerson.getLastName(), testPerson.getFirstName(),
         testPerson.getAge(), testPerson.getEyeColor(), testPerson.getGender(), person);
   }));
}

@TestFactory 注解将此方法标记为一个 DynamicTest 工厂(第 1 行),并根据 JUnit Jupiter 的要求返回 DynamicTest 实例的一个 Stream(第 2 行)。该 @TestFactory 所生成的测试不会执行任何花哨的操作;它们仅在 PersonDaoBean Spring bean 上调用 findById(第 6 行),并执行一些断言(第 10 和 11 行)。但它展示了如何创建一个动态测试。

标签和过滤

标签对过滤测试很有用。在本节中,我将介绍如何创建一个自定义过滤器,然后将它转换为一个组合注解,用于控制哪些测试可运行。

使用 @Tags

JUnit Jupiter 标签描述 @Tag 注解的用法,该注解创建一个新的标识符(标签),并接受单个 String 参数来唯一地标识该标签。下面给出了一些示例:

1
2
3
@Tag("foo")
@Tag("bar")
@Tag("advanced")

您可使用标签来注解方法或类,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Tag("advanced")
@TestFactory
@DisplayName("FindById - Dynamic Test Generator")
Stream< DynamicTest > generateFindByIdDynamicTests() {
   Long[] ids = { 1L, 2L, 3L, 4L, 5L, 6L };
   return Stream.of(ids).map(id -> dynamicTest("DynamicTest: Find by ID " + id, () -> {
     Person person = classUnderTest.findById(id);
     assertNotNull(person);
     int index = id.intValue() - 1;
     Person testPerson = PersonTestEnum.values()[index].getPerson();
     performPersonAssertions(testPerson.getLastName(), testPerson.getFirstName(),
         testPerson.getAge(), testPerson.getEyeColor(), testPerson.getGender(), person);
   }));
}

然后可使用 Maven POM 或 Gradle 构建脚本中的过滤器设置来过滤掉此测试。教程后面将介绍如何执行该操作。

创建您自己的组合注解

与使用 @Tag 和它的唯一名称相比, 使用@Tag 创建新的组合注解更重要。还记得上节中的 @Tag("advanced") 吗?我可以创建一个新的组合注解来表示一种高级测试类型,比如:

清单 6. 创建组合注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
 
import org.junit.jupiter.api.Tag;
 
@Retention(RUNTIME)
@Target({ TYPE, METHOD })
@Tag("advanced")
public @interface Advanced {
   // Nothing to do
}

现在,我在所有使用 @Tag("advanced") 的地方都使用 @Advanced 来代替,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Advanced
@TestFactory
@DisplayName("FindById - Dynamic Test Generator")
Stream< DynamicTest > generateFindByIdDynamicTests() {
   Long[] ids = { 1L, 2L, 3L, 4L, 5L, 6L };
   return Stream.of(ids).map(id -> dynamicTest("DynamicTest: Find by ID " + id, () -> {
     Person person = classUnderTest.findById(id);
     assertNotNull(person);
     int index = id.intValue() - 1;
     Person testPerson = PersonTestEnum.values()[index].getPerson();
     performPersonAssertions(testPerson.getLastName(), testPerson.getFirstName(),
         testPerson.getAge(), testPerson.getEyeColor(), testPerson.getGender(), person);
   }));
}

如前所述,您可在类级别或方法级别上使用新的组合注解(感谢 @Target 注解;参见清单 6 中的第 11 行)。可以查看示例应用程序中的 PersonDaoBeanRepeatedTest 类,看看这么做的实际效果,我在其中使用 @Advanced 注解了整个类。在 PersonDaoBeanTest 中,我只将生成动态测试的 generateFindByIdDynamicTests() 方法标记为 @Advanced

使用 Maven 运行

在第 1 部分中,我展示了如何使用 Maven 和 Gradle 运行 JUnit 测试。本节将展示如何配置 Maven POM,从示例应用程序中过滤掉 @Advanced 测试。

JUnit 用户指南包含各种 Maven 配置设置的更详细参考指南,所以如果需要更多信息,推荐您查阅该指南。

要试用它,首先需要运行构建,并注意运行的测试数量(下面第 12 行)。您应看到类似下面这样的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ mvn clean test
.
.
-------------------------------------------------------
  T E S T S
-------------------------------------------------------
May 22, 2017 10:04:15 AM org.junit.jupiter.engine.discovery.JavaElementsResolver resolveClass
.
.
Results :
 
Tests run: 92, Failures: 0, Errors: 0, Skipped: 0
 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 55.276 s
[INFO] Finished at: 2017-05-22T10:05:08-05:00
[INFO] Final Memory: 19M/297M
[INFO] ------------------------------------------------------------------------
$

记住运行了多少个测试(第 12 行),这样才能将该数字与应用过滤器后的值进行比较。

在 Eclipse 中打开 POM,找到 Maven surefire 插件:

1
2
3
4
5
6
7
8
9
10
11
< build >
     < plugins >
     .
     .
         < plugin >
             < artifactId >maven-surefire-plugin</ artifactId >
             < version >2.19</ version >
     .
     .
</ build >
     </ plugins >

现在修改 version 元素(第 7 行)下的 POM,使它类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
< build >
     < plugins >
     .
     .
         < plugin >
             < artifactId >maven-surefire-plugin</ artifactId >
             < version >2.19</ version >
             < configuration >
                 < properties >
                     < excludeTags >advanced</ excludeTags >
                 </ properties >
             </ configuration >
     .
     .
     </ plugins >
</ build >

再次运行构建内容,您应该看到运行的测试更少了。输出看起来应类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$mvn clean test
.
.
-------------------------------------------------------
  T E S T S
-------------------------------------------------------
May 22, 2017 10:09:42 AM org.junit.jupiter.engine.discovery.JavaElementsResolver resolveClass
.
.
Results :
 
Tests run: 47, Failures: 0, Errors: 0, Skipped: 0
 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 32.023 s
[INFO] Finished at: 2017-05-22T10:10:11-05:00
[INFO] Final Memory: 20M/300M
[INFO] ------------------------------------------------------------------------
$

可以注意到,应用过滤后(第 12 行),运行的测试数量比以前少多了。

使用 Gradle 运行

接下来,我将展示如何配置 Gradle 构建脚本,从示例应用程序中过滤掉 @Advanced 测试。

JUnit 用户指南包含各种 Gradle 配置设置的更详细参考指南,所以如果需要更多信息,推荐您查阅该指南。

要试用它,首先需要运行构建,并注意运行的测试数量(第 19、21 和 23 行)。您应看到类似下面这样的信息:

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
$ gradle clean test
:clean
:compileJava
:processResources NO-SOURCE
:classes
:compileTestJava
:processTestResources
:testClasses
:junitPlatformTest
.
.
Test run finished after 62083 ms
[        24 containers found      ]
[         0 containers skipped    ]
[        24 containers started    ]
[         0 containers aborted    ]
[        24 containers successful ]
[         0 containers failed     ]
[        92 tests found           ]
[         0 tests skipped         ]
[        92 tests started         ]
[         0 tests aborted         ]
[        92 tests successful      ]
[         0 tests failed          ]
 
:test SKIPPED
 
BUILD SUCCESSFUL
 
Total time: 1 mins 3.718 secs
$

记住运行了多少个测试(第 23 行),这样才能将该数字与应用过滤器后的值进行比较。

在 Eclipse 中打开构建脚本并找到 junitplatform 节,该节类似于:

1
2
3
4
5
6
7
8
9
junitPlatform {
   filters {
     engines {
     }
     tags {
     }
   }
   logManager 'org.apache.logging.log4j.jul.LogManager'
}

现在修改 tags 元素下的 POM,使它类似于:

1
2
3
4
5
6
7
8
9
10
junitPlatform {
   filters {
     engines {
     }
     tags {
         exclude 'advanced'
     }
   }
   logManager 'org.apache.logging.log4j.jul.LogManager'
}

再次运行构建内容,您应该看到运行的测试更少了。输出看起来应类似于:

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
$ gradle clean test
:clean
:compileJava
:processResources NO-SOURCE
:classes
:compileTestJava
:processTestResources
:testClasses
:junitPlatformTest
.
.
Test run finished after 38834 ms
[        13 containers found      ]
[         0 containers skipped    ]
[        13 containers started    ]
[         0 containers aborted    ]
[        13 containers successful ]
[         0 containers failed     ]
[        47 tests found           ]
[         0 tests skipped         ]
[        47 tests started         ]
[         0 tests aborted         ]
[        47 tests successful      ]
[         0 tests failed          ]
 
:test SKIPPED
 
BUILD SUCCESSFUL
 
Total time: 40.487 secs
$

注意在应用过滤(第 19、21、23)后,运行的测试比以前少了多少。

结束语

JUnit 5 教程的后半部分重点介绍了 JUnit Vintage 和 JUnit Jupiter 扩展模型。JUnit Vintage 提供了与 JUnit 3 和 JUnit 4 的后向兼容性,JUnit Jupiter 扩展模型支持针对第三方工具或自定义测试场景来扩展 JUnit Jupiter API。我总结了 JUnit Jupiter 中提供的新扩展点,然后通过一系列示例重点展示了 JUnit 5 中针对参数注入、参数化测试、动态测试和自定义注解等新的可扩展特性。

恭喜您!您已学完 JUnit 5 教程。如果尚未观看本教程的配套视频,可以看一下。该视频详细介绍了您目前学到的有关新 ParameterResolver 和 @ParameterizedTest 特性的知识,还展示了如何运行 HelloJUnit5Part2 源代码中所包含的控制台启动器。


  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值