开源技术 * IBM 微讲堂:Kubeflow 系列(观看回放 | 下载讲义) 了解详情

JUnit 5 Vintage 和 JUnit Jupiter 扩展模型

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

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

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

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

前提条件

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

  • 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 中各种包之间的关系。

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 树中。

HelloJunit5Part2 示例应用程序的 JUnit 3 测试用例
.
.
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 树中。

HelloJunit5Part2 示例应用程序的 JUnit 4 测试用例
.
.
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 扩展模型中的扩展点。

扩展点
接口 说明
AfterAllCallback 定义 API 扩展,希望在调用所有测试后让测试容器执行额外的行为。
AfterEachCallback 定义 API 扩展,希望在调用每个测试方法后让测试执行额外的行为。
AfterTestExecutionCallback 定义 API 扩展,希望在执行每个测试后让测试立即执行额外的行为。
BeforeAllCallback 定义 API 扩展,希望在调用所有测试前让测试容器执行额外的行为。
BeforeEachCallback 定义 API 扩展,希望在调用每个测试前让测试执行额外的行为。
BeforeTestExecutionCallback 定义 API 扩展,希望在执行每个测试前让测试立即执行额外的行为。
ParameterResolver 定义 API 扩展,希望在运行时动态解析参数。
TestExecutionExceptionHandler 定义 API 扩展,希望处理在测试执行期间抛出的异常。

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

创建扩展

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

public class MyBeforeEachCallbackExtension implements BeforeEachCallback {
  @Override
  public void beforeEach(ExtensionContext context) throws Exception {
    // Implementation goes here
  }
}

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

激活扩展

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

@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 个方法:

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 ,随后在调用测试方法时会使用该对象。

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

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 ,您只需实现该接口:

Person 对象的 ParameterResolver 扩展点实现
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 注解完成注册工作。

使用 ParameterResolver
@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 行)。

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

仅将 ParameterResolver 用于单个方法
@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 就不知道使用哪一个。最终会看到以下消息来表明这种模糊性:

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 的应用程序。

参数化测试

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

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

  • @ValueSource
  • @EnumSource
  • @MethodSource

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

@ValueSource

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

语法类似于:

@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 的类型相匹配。例如, intsint 数组匹配, stringsString 数组匹配,等等。

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

  • String
  • int
  • long
  • double

@EnumSource

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

语法类似于:

@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 ,可以指定您喜欢的任何复杂对象作为测试方法的参数类型。语法类似于:

@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);
}

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

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

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();
}

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

@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

@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 实例的 StreamCollectionIterableIterator

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

创建 @TestFactory

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

@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 参数来唯一地标识该标签。下面给出了一些示例:

@Tag("foo")
@Tag("bar")
@Tag("advanced")

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

@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") 吗?我可以创建一个新的组合注解来表示一种高级测试类型,比如:

创建组合注解
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 来代替,如下所示:

@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 行)。您应看到类似下面这样的信息:

$ 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 插件:

<build>
    <plugins>
    .
    .
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
    .
    .
</build>
    </plugins>

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

<build>
    <plugins>
    .
    .
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19</version>
            <configuration>
                <properties>
                    <excludeTags>advanced</excludeTags>
                </properties>
            </configuration>
    .
    .
    </plugins>
</build>

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

$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 行)。您应看到类似下面这样的信息:

$ 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 节,该节类似于:

junitPlatform {
  filters {
    engines {
    }
    tags {
    }
  }
  logManager 'org.apache.logging.log4j.jul.LogManager'
}

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

junitPlatform {
  filters {
    engines {
    }
    tags {
        exclude 'advanced'
    }
  }
  logManager 'org.apache.logging.log4j.jul.LogManager'
}

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

$ 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 中针对参数注入、参数化测试、动态测试和自定义注解等新的可扩展特性。