使用 JUnit 5 进行单元测试

发表于:2017-7-20 13:46

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:成富    来源:IBM

嵌套测试
  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 中的新功能。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号