您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
使用 JUnit 5 进行单元测试
 
 来源:ibm 发布于: 2017-3-22
   次浏览      
 

对于所有的 Java 开发人员来说,你可以没有听说过 Spring 或是 Hibernate 框架,但是一定听说过 JUnit。JUnit 作为 Java 单元测试的鼻祖与事实上的标准,在非常多的项目中被使用。即便新兴的单元测试框架,如 TestNG 等,不断出现,JUnit 的重要性仍然是不言而喻的。目前广泛使用的是 JUnit 4 版本,而 JUnit 即将迎来它的最新版本 JUnit 5。JUnit 5 在增加了很多的新特性的同时,又保持了对 JUnit 4 的向后兼容性。本文对 JUnit 5 进行了详细的介绍。

JUnit 5 简介

与之前的版本不同,JUnit 5 由三个不同的模块组成。第一个模块是 JUnit 平台,其主要作用是在 JVM 上启动测试框架。它定义了一个抽象的 TestEngine API 来定义运行在平台上的测试框架,同时还支持通过命令行、Gradle 和 Maven 来运行平台。第二个模块是 JUnit Jupiter,包含了 JUnit 5 最新的编程模型和扩展机制。第三个模块是 JUnit Vintage,允许在平台上运行 JUnit 3 和 JUnit 4 的测试用例。

JUnit 5 对 Java 运行环境的最低要求是 Java 8。可以在 Eclipse 和 IntelliJ IDEA 上运行 JUnit 5 测试。本文的示例基于 IntelliJ IDEA 上开发,并使用 Gradle 作为构建工具。不过目前 IDE 对 JUnit 5 的支持还比较有限,只有最新版本的 IntelliJ IDEA 原生支持,在其它 IDE 上需要使用命令行工具来运行。

编写测试用例

JUnit 5 对编写单元测试用例的方式做了一系列的改进,如下介绍。

JUnit 5 注解

JUnit 5 提供了一些常用的注解在编写测试用例的时候使用。其中的一些注解和 JUnit 4 的注解有相同的名称,不过所在的 Java 包变成了 org.junit.jupiter.api。常用的注解见表 1。

表 1. JUnit 5 常用注解

清单 1 中给出了使用这些注解编写的单元测试用例。

清单 1. 使用常用注解的单元测试用例

@DisplayName("Calculator")
public class CalculatorTest {

private Calculator calculator;

@BeforeAll
public static void init() {
System.out.println("Start testing");
}

@BeforeEach
public void create() {
this.calculator = new Calculator();
}

@AfterEach
public void destroy() {
this.calculator = null;
}

@AfterAll
public static void cleanup() {
System.out.println("Finish testing");
}

@Test
@DisplayName("Test 1 + 2 = 3")
public void testAdd() {
assertEquals(3, this.calculator.add(1, 2));
}

@Test
@DisplayName("Test 3 - 2 = 1")
public void testSubtract() {
assertEquals(1, this.calculator.subtract(3, 2));
}

@Disabled
@Test
@DisplayName("disabled test")
public void ignoredTest() {
System.out.println("This test is disabled");
}
}

在这些注解中,最实用的应该是@DisplayName。通过@DisplayName,开发人员可以为每个测试用例添加更具体的名字,更容易传达用例所要测试的内容。

通过@Tag 注解可以为测试类或方法添加标签,但是不同的标签只是通过字符串来进行区分,并不是类型安全的。一个拼写错误就可能造成标签没有被正确应用。更好的做法是使用类型安全的元注解(meta annotation)。编译器会对元注解标签的正确性进行验证,从而减少无意的错误。清单 2 中定义了元注解标签@Remote,对应于标签 remote。

清单 2. 元注解标签

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("remote")
public @interface Remote {
}

清单 3 中展示了@Remote 的用法。使用@Remote 的作用等同于@Tag("Remote"),为 testGetUser 方法添加了标签 remote。使用@Remote 不仅提高了代码的可读性,也可以避免无意的拼写错误带来的问题。

清单 3. 使用元注解标签

@DisplayName("Remote test")
public class RemoteTest {
@Test
@Remote
public void testGetUser() {
System.out.println("Get user");
}
}

JUnit 5 断言

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:

第一类是简单断言,用来对单个值进行简单的验证,常用的方法见表 2。

表 2. 常用的断言方法

这些方法都有多个重载方法,可以提供额外的消息来作为断言不满足时的提示消息,还可以接受 Java 8 中的 Supplier 接口来获取要判断的值和显示的消息。清单 4 中给出了简单断言的使用示例。

清单 4. 简单断言

@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);

assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);

assertFalse(1 > 2);
assertTrue(1 < 2);

assertNull(null);
assertNotNull(new Object());
}

第二类是通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等,如清单 5 所示。

清单 5. assertArrayEquals 方法的示例

@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

第三类是通过 assertAll 方法来判断一组断言是否满足。assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言,如清单 6 所示。

清单 6. assertAll 方法的示例

@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

第四类是通过 assertThrows 或 expectThrows 来判断是否抛出期望的异常类型。两个方法的参数都是所期望的异常类型和对应的 Executable 接口的实现对象,区别在于 expectThrows 方法会返回抛出的异常对象。在清单 7 中,1/0 会抛出 ArithmeticException 异常,assertThrows 用来验证这一点。

清单 7. assertThrows 和 expectThrows 方法的示例

@Test
@DisplayName("throws exception")
public void exception() {
assertThrows(ArithmeticException.class, () -> System.out.println(1 / 0));
}

第五类是 fail 方法,用来使一个测试方法失败。清单 8 中的测试会直接失败。

清单 8. 通过 fail 方法直接使得测试失败

@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}

JUnit 5 前置条件

JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。在清单 9 中,assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

清单 9. JUnit 5 前置条件

@DisplayName("Assumptions")
public class AssumptionsTest {
private final String environment = "DEV";

@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}

@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}

嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。清单 10 中给出了使用嵌套测试的示例,用来测试 HashMap 的功能。

清单 10. 嵌套测试

@DisplayName("Nested tests for HashMap")
public class MapNestedTest {
Map<String, Object> map;

@Nested
@DisplayName("when a new")
class WhenNew {
@BeforeEach
void create() {
map = new HashMap<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(map.isEmpty());
}

@Nested
@DisplayName("after adding a new entry")
class AfterAdd {
String key = "key";
Object value = "value";

@BeforeEach
void add() {
map.put(key, value);
}

@Test
@DisplayName("is not empty")
void isNotEmpty() {
assertFalse(map.isEmpty());
}

@Test
@DisplayName("returns value when getting by key")
void returnValueWhenGettingByKey() {
assertEquals(value, map.get(key));
}

@Nested
@DisplayName("after removing the entry")
class AfterRemove {
@BeforeEach
void remove() {
map.remove(key);
}

@Test
@DisplayName("is empty now")
void isEmpty() {
assertTrue(map.isEmpty());
}

@Test
@DisplayName("returns null when getting by key")
void returnNullForKey() {
assertNull(map.get(key));
}
}
}
}
}

依赖注入

在 JUnit 5 之前,标准的测试类和测试方法是不允许有额外的参数的。这个限制在 JUnit 5 被取消了。JUnit 5 除了提供内置的标准参数之外,还可以通过扩展机制来支持额外的参数。

当参数的类型是 org.junit.jupiter.api.TestInfo 时,JUnit 5 会在运行测试时提供一个 TestInfo 接口的对象。通过 TestInfo 接口,可以获取到当前测试的相关信息,包括显示名称、标签、测试类和测试方法,如清单 11 所示。

清单 11. TestInfo 依赖注入

@Test
@DisplayName("test info")
public void testInfo(final TestInfo testInfo) {
System.out.println(testInfo.getDisplayName());
}

当参数的类型是 org.junit.jupiter.api.TestReporter 时,在运行测试时,通过作为参数传入的 TestReporter 接口对象,来输出额外的键值对信息。这些信息可以被测试执行的监听器 TestExecutionListener 处理,也可以被输出到测试结果报告中,如清单 12 所示。

清单 12. TestReporter 依赖注入

@Test
@DisplayName("test reporter")
public void testReporter(final TestReporter testReporter) {
testReporter.publishEntry("name", "Alex");
}

除了 TestInfo 和 TestReporter 之外,也可以通过 JUnit 5 的扩展机制来添加对其他类型参数的支持。将在下面关于 JUnit 5 扩展机制的一节中进行介绍。

动态测试

目前所介绍的 JUnit 5 测试方法的创建都是静态的,在编译时刻就已经存在。JUnit 5 新增了对动态测试的支持,可以在运行时动态创建测试并执行。通过动态测试,可以满足一些静态测试无法解的需求,也可以完成一些重复性很高的测试。比如,有些测试用例可能依赖运行时的变量,有时候会需要生成上百个不同的测试用例。这些场景都是动态测试可以发挥其长处的地方。动态测试是通过新的@TestFactory 注解来实现的。测试类中的方法可以添加@TestFactory 注解的方法来声明其是创建动态测试的工厂方法。这样的工厂方法需要返回 org.junit.jupiter.api.DynamicTest 类的集合,可以是 Stream、Collection、Iterable 或 Iterator 对象。每个表示动态测试的 DynamicTest 对象由显示名称和对应的 Executable 接口的实现对象来组成。清单 13 中展示了@TestFactory 的示例。

清单 13. 动态测试

@TestFactory
public Collection<DynamicTest> simpleDynamicTest() {
return Collections.singleton(dynamicTest
("simple dynamic test", () -> assertTrue(2 > 1)));
}

DynamicTest 提供了一个静态方法 stream 来根据输入生成动态测试,如清单 14 所示。

清单 14. 通过 stream 方法来生成动态测试

@TestFactory
public Stream<DynamicTest> streamDynamicTest() {
return stream(
Stream.of("Hello", "World").iterator(),
(word) -> String.format("Test - %s", word),
(word) -> assertTrue(word.length() > 4)
);
}

执行测试用例

JUnit 5 提供了三种不同的方式来执行测试用例,分别是通过 Gradle 插件、Maven 插件和命令行来运行。

Gradle

JUnit 5 提供了 Gradle 插件,在 Gradle 项目中运行单元测试,如清单 15 所示。

清单 15. 使用 JUnit 5 的 Gradle 插件

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-M2'
}
}

apply plugin: 'org.junit.platform.gradle.plugin'

在启用了 Gradle 插件之后,可以通过 junitPlatformTest 任务来运行单元测试。可以在 Gradle 脚本中对插件进行定制,如通过 reportsDir 设置测试结果报告的生成路径,通过 tags 来设置包含或排除的标签名称,如清单 16 所示。

清单 16. 配置 JUnit 5 的 Gradle 插件

junitPlatform {
platformVersion 1.0
reportsDir "build/test-results/junit-platform"
tags {
include 'fast', 'smoke'
}
}

Maven

在 Maven 项目中可以通过 Surefire 插件来运行 JUnit 5 测试,只需要在 POM 文件中进行配置即可。如清单 17 所示。

清单 17. 在 Maven 项目中使用 JUnit 5

<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.0.0-M2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

命令行

除了 Gradle 和 Maven 之外,还可以通过命令行来运行 JUnit 5 测试。只需要直接运行 Java 类 org.junit.platform.console.ConsoleLauncher 即可。ConsoleLauncher 提供了不同的命令行参数来配置测试运行的行为,如-n 来指定包含的 Java 类名满足的模式,-t 来包含标签,-T 来排除标签。

扩展机制

JUnit 5 提供了标准的扩展机制来允许开发人员对 JUnit 5 的功能进行增强。JUnit 5 提供了很多的标准扩展接口,第三方可以直接实现这些接口来提供自定义的行为。通过@ExtendWith 注解可以声明在测试方法和类的执行中启用相应的扩展。

扩展的启用是继承的,这既包括测试类本身的层次结构,也包括测试类中的测试方法。也就是说,测试类会继承其父类中的扩展,测试方法会继承其所在类中的扩展。除此之外,在一个测试上下文中,每一个扩展只能出现一次。

创建扩展

JUnit 5 中的扩展非常容易创建,只是实现了特定接口的 Java 类。JUnit 5 的扩展都需要实现 org.junit.jupiter.api.extension.Extension 接口,不过该接口只是一个标记接口,并没有任何需要实现的具体方法。真正起作用的是 Extension 的子接口,作为 JUnit 5 提供的扩展点。

测试执行条件

ContainerExecutionCondition 和 TestExecutionCondition 接口用来配置是否启用测试类或测试方法。前面提到的@Disabled 注解也是通过这样的机制来实现的。ContainerExecutionCondition 接口对应的是测试类,而 TestExecutionCondition 接口对应的是测试方法。

ContainerExecutionCondition 接口的 evaluate 方法接受 ContainerExtensionContext 接口作为参数,并返回 ConditionEvaluationResult 类的对象作为结果。通过 ContainerExtensionContext 接口可以获取到当前测试类的上下文信息,而 ConditionEvaluationResult 类则表示该测试类是否被启用。

TestExecutionCondition 接口也是包含一个 evaluate 方法,只不过参数类型是 TestExtensionContext,其返回结果也是 ConditionEvaluationResult 类的对象。

通过扩展的方式禁用的测试类和方法,可以通过 JVM 参数 junit.conditions.deactivate 来重新启用,只需要把相应的条件类禁用即可。

清单 18 中扩展 DisableAPITests 实现了 ContainerExecutionCondition 和 TestExecutionCondition 接口,当测试类或方法中包含标签 api 时,通过 ConditionEvaluationResult.disabled()表示对其禁用。

清单 18. 测试执行条件扩展示例

public class DisableAPITests implements ContainerExecutionCondition, 
TestExecutionCondition {
@Override
public ConditionEvaluationResult evaluate
(final ContainerExtensionContext context) {
return checkTags(context.getTags());
}

@Override
public ConditionEvaluationResult evaluate
(final TestExtensionContext context) {
return checkTags(context.getTags());
}

private ConditionEvaluationResult checkTags
(final Set<String> tags) {
if (tags.contains("api")) {
return ConditionEvaluationResult.disabled("No API tests!");
}
return ConditionEvaluationResult.enabled("");
}
}

清单 19 中的测试类的 simpleAPITest 方法使用了标签 api,在执行时会被禁用。

清单 19. 使用 DisableAPITests 的测试用例

@ExtendWith(DisableAPITests.class)
public class APITests {
@Test
@Tag("api")
public void simpleAPITest() {
System.out.println("simple API test");
}
}

后处理测试实例

通过 TestInstancePostProcessor 可以对测试实例添加后处理的逻辑,从而进一步对实例进行定制,比如可以通过依赖注入的方式来设置其中的属性,或是添加额外的初始化逻辑等。

在清单 20 中,扩展 InjectAPIEnv 实现了 TestInstancePostProcessor 接口,在 postProcessTestInstance 方法中通过 Commons Lang 中的 MethodUtils.invokeMethod 来调用当前测试实例中的 setEnv 方法,并设置为 DEV。

清单 20. 后处理测试实例的示例

public class InjectAPIEnv implements TestInstancePostProcessor {
@Override
public void postProcessTestInstance
(final Object testInstance,
final ExtensionContext context) throws Exception {
MethodUtils.invokeMethod(testInstance, "setEnv", "DEV");
}
}

清单 21 中给出了使用该扩展的示例。

清单 21. 使用后处理测试实例的示例

@ExtendWith(InjectAPIEnv.class)
public class APITests {
private String env;

public void setEnv(final String env) {
this.env = env;
}

@Test
public void showInjected() {
assertEquals("DEV", this.env);
}
}

参数解析

在之前介绍 JUnit 5 的参数解析时,提到了 JUnit 5 可以自动解析 TestInfo 和 TestReporter 类型的参数。除了这两种类型的参数之外,也可以通过扩展 ParameterResolver 接口来提供自定义的参数解析功能。ParameterResolver 接口中有两个方法,分别是 supports 和 resolve。两个方法的参数是一样的,分别是 ParameterContext 和 ExtensionContext 接口的对象。通过 ParameterContext 可以获取到需要解析的参数的信息,而 ExtensionContext 接口可以获取到当前测试类或方法的上下文信息。

清单 22. 参数解析的示例

public class APIEnvResolver implements ParameterResolver {
@Override
public boolean supports
(final ParameterContext parameterContext,
final ExtensionContext extensionContext) throws
ParameterResolutionException {
return parameterContext.getParameter().getType() == String.class
&& parameterContext.getIndex() == 0;
}

@Override
public Object resolve(final ParameterContext parameterContext,
? final ExtensionContext extensionContext)
throws ParameterResolutionException {
return "DEV";
}
}

清单 23 给出了使用参数解析扩展的示例。

清单 23. 使用参数解析扩展的示例

@ExtendWith(APIEnvResolver.class)
public class APITests {
@Test
public void showResolved(final String env) {
assertEquals("DEV", env);
}
}

测试执行回调方法

JUnit 5 提供了一系列与测试执行过程相关的回调方法,在测试执行中的不同阶段,运行自定义的逻辑。这些回调方法可以用来做一些与日志和性能分析的任务。具体的回调方法和描述见表 3。

表 3. 测试执行中的回调方法

清单 24 中给出了使用测试执行中的回调方法的示例。

清单 24. 使用测试执行回调方法的示例

public class Timing implements BeforeTestExecutionCallback, 
AfterTestExecutionCallback {
@Override
public void beforeTestExecution
(final TestExtensionContext context) throws Exception {
getStore(context).put
(context.getTestMethod().get(), System.currentTimeMillis());
}

@Override
public void afterTestExecution
(final TestExtensionContext context) throws Exception {
final Method testMethod = context.getTestMethod().get();
final long start = getStore(context).remove(testMethod, long.class);
final long duration = System.currentTimeMillis() - start;
context.publishReportEntry(ImmutableMap.of
(testMethod.getName(), Long.toString(duration)));
}

private Store getStore(TestExtensionContext context) {
return context.getStore(Namespace.create(getClass(), context));
}
}

异常处理

通过 TestExecutionExceptionHandler 接口可以对测试运行中抛出的异常进行处理。可以在运行中忽略某些异常,或是在特定类型的异常发生时执行某些处理动作,如可以在出现数据库异常时回滚事务。清单 25 给出了异常处理的示例。

清单 25. 异常处理的示例

public class IgnoreNullPointerException implements 
TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException
(final TestExtensionContext context,
final Throwable throwable) throws Throwable {
if (throwable instanceof NullPointerException) {
return;
}
throw throwable;
}
}

迁移指南

JUnit 平台可以通过 Jupiter 引擎来运行 JUnit 5 测试,Vintage 引擎来运行 JUnit 3 和 JUnit 4 测试。因此,已有的 JUnit 3 和 4 的测试不需要任何修改就可以直接在 JUnit 平台上运行。只需要确保 Vintage 引擎的 jar 包出现在 classpath 中,JUnit 平台会自动发现并使用该引擎来运行 JUnit 3 和 4 测试。开发人员可以按照自己的项目安排来规划迁移到 JUnit 5 的进度。可以保持已有的 JUnit 3 和 4 的测试用例不变,而新增加的测试用例则使用 JUnit 5。

在进行迁移的时候需要注意如下的变化:

注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。

把@Before 和@After 替换成@BeforeEach 和@AfterEach。

把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。

把@Ignore 替换成@Disabled。

把@Category 替换成@Tag。

把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

小结

单元测试是应用程序不可或缺的一部分。作为 Java 开发中单元测试的事实标准,JUnit 被广泛使用。本文详细介绍了在 JUnit 5 中编写和运行测试用例的方式,并对新的扩展机制做了详细介绍。在编写测试用例方面,本文介绍了 JUnit 5 中新的注解、断言和前置条件,以及对于嵌套测试、依赖注入和动态测试的支持。在运行测试用例方面,详细介绍了通过 Gradle、Maven 和命令行来运行 JUnit 5 测试。扩展机制作为 JUnit 5 的一大亮点,本文详细介绍了如何通过扩展来添加测试执行条件、后处理测试实例、解析测试和处理异常等。开发人员可以现在就尝试 JUnit 5 中的新功能。

   
次浏览       
相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...