转自: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 Jupiter API 中的注解、断言和前置条件
本教程介绍 JUnit 5。我们首先介绍如何在您的计算机上安装并设置 JUnit 5。我将简要介绍 JUnit 5 的架构和组件,然后展示如何使用 JUnit Jupiter API 中的新注解、断言和前置条件。
在第 2 部分中,我们将更深入地介绍 JUnit 5,包括新的 JUnit Jupiter 扩展模型、参数注入、动态测试等。
在本教程中,我使用了 JUnit 5, Milestone 5。
出于本教程的目的,我假设您熟悉以下软件的使用:
要跟随示例进行操作,您应在计算机上安装 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 版本都是整体式的。除了在 4.4 版中包含 Hamcrest JAR,JUnit 基本来讲就是一个很大的 JAR 文件。测试内容编写者 — 像您我这样的开发人员 — 和工具供应商都使用它的 API,但后者使用很多内部 JUnit API。
大量使用内部 API 给 JUnit 的维护者造成了一些麻烦,并且留给他们推动该技术发展的选择余地不多。来自 JUnit 5 用户指南:
“在 JUnit 4 中,只有外部扩展编写者和工具构建者才使用最初作为内部结构而添加的许多功能。这让更改 JUnit 4 变得特别困难,有时甚至根本不可能。”
JUnit Lambda(现在称为 JUnit 5)团队决定将 JUnit 重新设计为两个明确且不同的关注区域:
这些关注区域现在已整合到 JUnit 5 的架构中,并且它们是明确分离的。图 1 演示了新架构(图像来自 Nicolai Parlog):
如果仔细查看图 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 规范的测试框架(包括 JUnit Jupiter)都包含两个组件:
TestEngine
实现。对于本教程,前者是 JUnit Jupiter API,后者是 JUnit Jupiter Test Engine。我将介绍这二者。
作为开发人员,您将使用 JUnit Jupiter API 创建单元测试来测试您的应用程序代码。使用该 API 的基本特性 — 注解、断言等 — 是本部分教程的主要关注点。
JUnit Jupiter API 的设计让您可通过插入各种生命周期回调来扩展它的功能。您将在第 2 部分中了解如何使用这些回调完成有趣的工作,比如运行参数化测试,将参数传递给测试方法,等等。
您将使用 JUnit Jupiter Test Engine 发现和执行 JUnit Jupiter 单元测试。该测试引擎实现了 JUnit Platform 中包含的 TestEngine
接口。可将 TestEngine
看作单元测试与用于启动它们的工具(比如 IDE)之间的桥梁。
在 JUnit 术语中,运行单元测试的过程分为两部分:
用于发现测试和创建测试计划的 API 包含在 JUnit Platform 中,由一个 TestEngine
实现。该测试框架将测试发现功能封装到其 TestEngine
实现中。JUnit Platform 负责使用 IDE 和构建工具(比如 Gradle 和 Maven)发起测试发现流程。
测试发现的目的是创建测试计划,该计划中包含一个测试规范。测试规范包含以下组件:
测试计划是根据测试规范所发现的所有测试类、这些类中的测试方法、测试引擎等的分层视图。测试计划准备就绪后,就可以执行了。
用于执行测试的 API 包含在 JUnit Platform 中,由一个或多个 TestEngine
实现。测试框架将测试执行功能封装在它们的 TestEngine
实现中,但 JUnit Platform 负责发起测试执行流程。通过 IDE 和构建工具(比如 Gradle 和 Maven)发起测试执行工作。
一个名为 Launcher
的 JUnit Platform 组件负责执行在测试发现期间创建的测试计划。某个流程 — 假设是您的 IDE — 通过 JUnit Platform(具体来讲是 junit-platform-launcher
API)发起测试执行流程。这时,JUnit Platform 将测试计划连同 TestExecutionListener
一起传递给 Launcher
。TestExecutionListener
将报告测试执行结果,从而在您的 IDE 中显示该结果。
测试执行流程的目的是向用户准确报告在测试运行时发生了哪些事件。这包括测试成功和失败报告,以及伴随失败而生成的消息,帮助用户理解所发生的事件。
许多组织对 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 包之间的依赖关系。
支持 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 4 开始,注解 (annotation) 就成为测试框架的核心特性,这一趋势在 JUnit 5 中得以延续。我无法介绍 JUnit 5 的所有注解,本节仅简要介绍最常用的注解。
首先,我将比较 JUnit 4 中与 JUnit 5 中的注解。JUnit 5 团队更改了一些注解的名称,让它们更直观,同时保持功能不变。如果您正在使用 JUnit 4,下表将帮助您适应这些更改。
接下来看看一些使用这些注解的示例。尽管一些注解已在 JUnit 5 中重命名,但如果您使用过 JUnit 4,应熟悉它们的功能。清单 1 中的代码来自 JUnit5AppTest.java
,可在 HelloJUnit5 示例应用程序中找到。
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).");
}
.
.
}
|
看看上面突出显示行中的注解:
@RunWith
连同它的参数 JUnitPlatform.class
(一个基于 JUnit 4 且理解 JUnit Platform 的 Runner
)让您可以在 Eclipse 内运行 JUnit Jupiter 单元测试。Eclipse 尚未原生支持 JUnit 5。未来,Eclipse 将提供原生的 JUnit 5 支持,那时我们不再需要此注解。@DisplayName
告诉 JUnit 在报告测试结果时显示 String
“Testing using JUnit 5”,而不是测试类的名称。@BeforeAll
告诉 JUnit 在运行这个类中的所有 @Test
方法之前运行 init()
方法一次。@AfterAll
告诉 JUnit 在运行这个类中的所有 @Test
方法之后运行 done()
方法一次。@BeforeEach
告诉 JUnit 在此类中的每个@Test
方法之前运行 setUp()
方法。@AfterEach
告诉 JUnit 在此类中的每个@Test
方法之后运行 tearDown()
方法。@Test
告诉 JUnit,aTest()
方法是一个 JUnit Jupiter 测试方法。@Disabled
告诉 JUnit 不运行此 @Test
方法,因为它已被禁用。 断言 (assertion) 是 org.junit.jupiter.api.Assertions
类上的众多静态方法之一。断言用于测试一个条件,该条件必须计算为 true
,测试才能继续执行。
如果断言失败,测试会在断言所在的代码行上停止,并生成断言失败报告。如果断言成功,测试会继续执行下一行代码。
表 2 中列出的所有 JUnit Jupiter 断言方法都接受一个可选的 message
参数(作为最后一个参数),以显示断言是否失败,而不是显示标准的缺省消息。
清单 2 给出了一个使用这些断言的示例,该示例来自 HelloJUnit5 示例应用程序。
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);
.
.
}
|
看看上面突出显示行中的断言:
assertEquals
:如果第一个参数值 (4) 不等于第二个参数值 (2+2),则断言失败。在报告断言失败时使用用户提供的消息(该方法的第 3 个参数)。assertFalse
:表达式 nullValue != null
必须为 false
,否则断言失败。assertNull
:nullValue
参数必须为 null
,否则断言失败。assertNotNull
:String
文字值 “A String” 不得为 null
,否则断言失败并报告消息 “INCONCEIVABLE!”(而不是缺省的 “Assertion failed” 消息)。assertTrue
:如果表达式 nullValue == null
不等于 true
,则断言失败。除了支持这些标准断言,JUnit Jupiter AP 还提供了多个新断言。下面介绍其中的两个。
清单 3 中的 @assertAll()
方法给出了清单 2 中看到的相同断言,但包装在一个新的断言方法中:
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 中的代码中,如果任何断言失败,测试就会在该位置失败,意味着不会执行任何其他断言。
在某些条件下,接受测试的类应抛出异常。JUnit 4 通过 expected =
方法参数或一个 @Rule
提供此能力。与此相反,JUnit Jupiter 通过 Assertions
类提供此能力,使它与其他断言更加一致。
我们将所预期的异常视为可以进行断言的另一个条件,因此 Assertions
包含处理此条件的方法。清单 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 允许您创建嵌套的类,以保持测试代码更清晰,这有助于让测试结果更易读。通过在主类中创建嵌套的测试类,可以创建更多的名称空间,这提供了两个主要优势:
testMethodButOnlyUnderThisOrThatCondition_2()
的方法名)。从 JUnit Jupiter 开始,只有嵌套类中的方法必须具有唯一的名称。清单 6 展示了这一优势。
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
中以嵌套的形式显示。
能编写单元测试很不错,但如果不能运行它们,就没有什么意义了。本节展示如何在 Eclipse 中运行 JUnit 测试,首先使用 Maven,然后从命令行使用 Gradle。
下面的视频展示了如何从 GitHub 克隆示例应用程序代码,并在 Eclipse 中运行测试。在该视频中,我还展示了如何从命令行以及 Eclipse 内使用 Maven 和 Gradle 运行单元测试。Eclipse 对 Maven 和 Gradle 都提供了很好的支持。
应用 3 种工具运行单元测试
点击查看视频演示查看抄本
下面将提供一些简要的说明,但该视频提供了更多细节。观看该视频,了解如何:
要理解教程的剩余部分,您需要从 GitHub 克隆示例应用程序。为此,可打开一个终端窗口 (Mac) 或命令提示 (Windows),导航到您希望放入代码的目录,然后输入以下命令:
git clone https://github.com/makotogo/HelloJUnit5
|
现在您的机器上已拥有该代码,可以在 Eclipse IDE 内运行 JUnit 测试了。接下来介绍如何运行测试。
如果您已跟随该视频进行操作,应该已将代码导入 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 视图将会出现。您现在已准备好完成本教程的练习。
打开一个终端窗口 (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] ------------------------------------------------------------------------
|
打开一个终端窗口 (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 部分的最后一节,您将完成以下任务:
App
类,让您的单元测试通过检查。采用真正的测试驱动开发 (TDD) 方式,首先编写单元测试,运行它们,并会观察到它们全部失败了。然后编写实现,直到单元测试通过,这时您就大功告成了。
注意,JUnit5AppTest
类仅提供了两个现成的测试方法。首次运行该类时,二者都是 “绿色” 的。要完成这些练习,您需要添加剩余的代码,包括用于告诉 JUnit 运行哪些测试方法的注解。记住,如果没有正确配备一个类或方法,JUnit 将跳过它。
如果遇到困难,请查阅 com.makotojava.learn.hellojunit5.solution
包来寻找解决方案。
首先从 JUnit5AppTest.java
开始。打开此文件并按照 Javadoc 注解中的指示操作。
提示:使用 Eclipse 中的 Javadoc 视图读取测试指令。要打开 Javadoc 视图,可以转到 Window > Show View > Javadoc。您应该看到 Javadoc 视图。根据您设置工作区的方式,该窗口可能出现在任意多个位置。在我的工作区中,该窗口与图 3 中的屏幕截图类似,出现在 IDE 右侧的编辑器窗口下方:
编辑器窗口中显示了具有原始 HTML 标记的 Javadoc 注解,但在 Javadoc 窗口中,已将其格式化,因此更易于阅读。
如果您像我一样,您会使用 IDE 执行以下工作:
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。
App
的单一 add()
方法提供的功能很容易理解,而且在设计上非常简单。我不希望复杂应用程序的业务逻辑阻碍您对 JUnit Jupiter 的学习。
单元测试通过后,您就大功告成了!记住,如果遇到困难,可以在 com.makotojava.learn.hellojunit5.solution
包中查找解决方案。
在 JUnit 5 教程的前半部分中,我介绍了 JUnit 5 的架构和组件,并详细介绍了 JUnit Jupiter API。我们逐个介绍了 JUnit 5 中最常用的注解、断言和前置条件,而且通过一个快速练习演示了如何在 Eclipse、Maven 和 Gradle 中运行测试。
在第 2 部分中,您将了解 JUnit 5 的一些高级特性:
那么您接下来会怎么做?
第 2 部分
了解用于参数注入、参数化测试、动态测试和自定义注解的 JUnit Jupiter 扩展
在本教程的第 1 部分中,我介绍了 JUnit 5 的设置说明,以及 JUnit 5 的架构和组件。还介绍了如何使用 JUnit Jupiter API 中的新特性,包括注解、断言和前置条件。
在本部分中,您将熟悉组成全新 JUnit 5 的另外两个模块:JUnit Vintage 和 JUnit Jupiter 扩展模型。我将介绍如何使用这些组件实现参数注入、参数化测试、动态测试和自定义注解等。
与第 1 部分中一样,我将介绍如何使用 Maven 和 Gradle 运行测试。
请注意,本教程的示例基于 JUnit 5, Milestone 5。
假设您熟悉以下软件的使用:
要跟随示例进行操作,您应在计算机上安装 JDK 8、Eclipse、Maven、Gradle(可选)和 Git。如果缺少其中的任何工具,可使用下面的链接下载和安装它们:
升级到新的重要软件版本始终存在风险,但是在这里,升级不仅是个好主意,而且还很安全。
因为许多组织对 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 Vintage 旨在提供一条通往 JUnit Jupiter 的 “平稳升级路径”。两个 JUnit 5 模块依赖于 JUnit Vintage:
Runner
,允许在 JUnit 4 环境(比如 Eclipse)中执行测试。Rule
。JUnit Vintage 本身由两个模块组成:
因为 JUnit Platform 允许多个测试引擎同时运行,所以可让您的 JUnit 3 和 JUnit 4 测试与使用 JUnit Jupiter 编写的测试并列运行。教程后面将介绍如何执行该操作。
在 Eclipse、Maven 和 Gradle 中运行测试之前,我们花点时间复习一下基本单元测试的概念。我们将分析在 JUnit 3 和 JUnit 4 中编写的测试。
使用 JUnit 3 编写的测试将按原样在 JUnit Platform 上运行。只需将 junit-vintage
依赖项包含在构建版本中,其他部分就能直接运行。
在示例应用程序中,您将看到已包含在示例应用程序中的 Maven POM (pom.xml
) 和 Gradle 构建文件 (build.gradle
),所以您可立即运行这些测试。
清单 1 给出了示例应用程序的一个 JUnit 3 测试的部分内容。它位于 com.makotojava.learn.junit3
包中的 src/test/java
树中。
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 Platform 上运行。只需将 junit-vintage
依赖项包含在构建版本中,就能直接运行它。
示例应用程序中包含的 Maven POM 和 Gradle 构建文件 (build.gradle
) 中已包含该依赖项,所以您可立即运行这些测试。
清单 2 给出了示例应用程序的一个 JUnit 4 测试的部分内容。它位于 com.makotojava.learn.junit4
包中的 src/test/java
树中。
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-migration-support
包中包含了用于后向兼容性的一些选定 Rule
,所以如果您对 JUnit 4 规则进行了大力投资也不用担心。在 JUnit 5 中,您将使用 JUnit Jupiter 扩展模型实现 JUnit 4 中的各种规则提供的相同行为。下一节将介绍如何完成该工作。
通过使用 JUnit 扩展模型,现在任何开发人员或工具供应商都能扩展 JUnit 的核心功能。
要想真正认识到 JUnit Jupiter 扩展模型的开创性,需要理解它如何扩展 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 Jupiter API,首先从扩展点开始。
一个扩展点对应于 JUnit test 生命周期中一个预定义的点。从 Java™ 语言的角度讲,扩展点是您实现并向 JUnit 注册(激活)的回调接口。因此,扩展点是回调接口,扩展是该接口的实现。
在本教程中,我将把已实现的扩展点回调接口称为扩展。
一旦注册您的扩展,就会将其激活。在测试生命周期中合适的点上,JUnit 将使用回调接口调用它。
表 1 总结了 JUnit Jupiter 扩展模型中的扩展点。
表 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
方法。您如何完成该工作?下面我们就学习一下。
如果所编写的测试方法在其签名中包含一个参数,则必须将该参数解析为一个实际对象,然后 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
,您只需实现该接口:
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
,必须向 JUnit Jupiter 测试引擎注册它。与前面的演示一样,可使用 @ExtendWith
注解完成注册工作。