内容


JUnit 5 简介,第 2 部分

JUnit 5 Vintage 和 JUnit Jupiter 扩展模型

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

Comments

系列内容:

此内容是该系列 2 部分中的第 # 部分: JUnit 5 简介,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:JUnit 5 简介,第 2 部分

敬请期待该系列的后续内容。

在本教程的第 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 中各种包之间的关系。

图 1. 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 树中。

清单 1. 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 树中。

清单 2. 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 扩展模型中的扩展点。

表 1. 扩展点
接口说明
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,您只需实现该接口:

清单 3. 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 注解完成注册工作。

清单 4. 使用 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 行)。

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

清单 5. 仅将 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 的应用程序。

JUnit5 的高级特性

JUnit5 的高级特性
JUnit5 的高级特性

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

参数化测试

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

清单 6. 创建组合注解
                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 中针对参数注入、参数化测试、动态测试和自定义注解等新的可扩展特性。

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


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=1048577
ArticleTitle=JUnit 5 简介,第 2 部分: JUnit 5 Vintage 和 JUnit Jupiter 扩展模型
publish-date=12182017