JUnit 5中扩展模型的深入理解

什么是Junit5 ?

先看来个公式:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

这看上去比Junit4 复杂,实际上在导入包时也会复杂一些。

JUnit Platform是在JVM上启动测试框架的基础。

JUnit Jupiter是JUnit5扩展的新的编程模型和扩展模型,用来编写测试用例。Jupiter子项目为在平台上运行Jupiter的测试提供了一个TestEngine (测试引擎)。

JUnit Vintage提供了一个在平台上运行JUnit 3和JUnit 4的TestEngine 。

关键要点

  • JUnit 5是一个模块化和可扩展的测试框架,支持Java 8及更高版本。
  • JUnit 5由三个部分组成——一个基础平台、一个新的编程和扩展模型Jupiter,以及一个名为Vintage的向后兼容的测试引擎。
  • JUnit 5 Jupiter的扩展模型可用于向JUnit中添加自定义功能。
  • 扩展模型API测试生命周期提供了钩子和注入自定义参数的方法(即依赖注入)。

JUnit是最受欢迎的基于JVM的测试框架,在第5个主要版本中进行了彻底的改造。JUnit 5提供了丰富的功能——从改进的注解、标签和过滤器到条件执行和对断言消息的惰性求值。这让基于TDD编写单元测试变得轻而易举。新框架还带来了一个强大的扩展模型。扩展开发人员可以使用这个新模型向JUnit 5中添加自定义功能。本文将指导你完成自定义扩展的设计和实现。这种自定义扩展机制为Java程序员提供了一种创建和执行故事和行为(即BDD规范测试)的方法。

我们首先使用JUnit 5和我们的自定义扩展(称为“StoryExtension”)来编写一个示例故事和行为(测试方法)。这个示例使用了两个新的自定义注解“@Story”和“@Scenario”,以及“Scene”类,用以支持我们的自定义StoryExtension:

import org.junit.jupiter.api.extension.ExtendWith;

import ud.junit.bdd.ext.Scenario;
import ud.junit.bdd.ext.Scene;
import ud.junit.bdd.ext.Story;
import ud.junit.bdd.ext.StoryExtension; 

@ExtendWith(StoryExtension.class)
@Story(name=“Returns go back to the stockpile”, description=“...“)
public class StoreFrontTest {

 @Scenario(“Refunded items should be returned to the stockpile”)
 public void refundedItemsShouldBeRestocked(Scene scene) {
  scene
   .given(“customer bought a blue sweater”,
      () -> buySweater(scene, “blue”))

   .and(“I have three blue sweaters in stock”,
      () -> assertEquals(3, sweaterCount(scene, “blue”),
        “Store should carry 3 blue sweaters”))

   .when(“the customer returns the blue sweater for a refund”,
      () -> refund(scene, 1, “blue”))

   .then(“I should have four blue sweaters in stock”,
      () -> assertEquals(4, sweaterCount(scene, “blue”),
        “Store should carry 4 blue sweaters”))
   .run();
 }
}

从代码片段中我们可以看到,Jupiter的扩展模型非常强大。我们还可以看到,我们的自定义扩展及其相应的注解为测试用例编写者提供了简单而干净的方法来编写BDD规范。

作为额外的奖励,当使用我们的自定义扩展程序执行测试时,会生成如下所示的文本报告:

STORY: Returns go back to the stockpile
 
As a store owner, in order to keep track of stock, I want to add items back to stock when they're returned.
 
SCENARIO: Refunded items should be returned to stock
   GIVEN that a customer previously bought a blue sweater from me
     AND I have three blue sweaters in stock
    WHEN the customer returns the blue sweater for a refund
    THEN I should have four blue sweaters in stock

这些报告可以作为应用程序功能集的文档。

自定义扩展StoryExtension能够借助以下核心概念来支持和执行故事和行为:

  • 用于装饰测试类和测试方法的注解
  • JUnit 5 Jupiter的生命周期回调
  • 动态参数解析

注解

示例中的“@ExtendWith”注解是由Jupiter提供的标记接口。这是在测试类或方法上注册自定义扩展的方法,目的是让Jupiter测试引擎调用给定类或方法的自定义扩展。或者,测试用例编写者可以通过编程的方式注册自定义扩展,或者通过服务加载器机制进行自动注册。

我们的自定义扩展需要一种识别故事的方法。为此,我们定义了一个名为“Story”的自定义注解类,如下所示:

import org.junit.platform.commons.annotation.Testable;

@Testable
public @interface Story {...}

测试用例编写者应该使用这个自定义注解将测试类标记为故事。请注意,这个注解本身使用了JUnit 5内置的“@Testable”注解。这个注解为IDE和其他工具提供了一种识别可测试的类和方法的方式——也就是说,带有这个注解的类或方法可以通过JUnit 5 Jupiter测试引擎来执行。

我们的自定义扩展还需要一种方法来识别故事中的行为或场景。为此,我们定义一个名为“Scenario”的自定义注解类,看起来像这样:

import org.junit.jupiter.api.Test;

@Test
public @interface Scenario {...}

测试用例编写者应使用这个自定义注解将测试方法标记为场景。这个注解本身使用了JUnit 5 Jupiter的内置“@Test”注解。当IDE和测试引擎扫描给定的一组测试类并在公共实例方法上找到@Scenario注解时,就会将这些方法标记为可执行的测试方法。

请注意,与JUnit 4的@Test注解不同,Jupiter的@Test注解不支持可选的“预期”异常和“超时”参数。Jupiter的@Test注解是从头开始设计的,并考虑到了可扩展性。

生命周期

JUnit 5 Jupiter提供了扩展回调,可用于访问测试生命周期事件。扩展模型提供了几个接口,用于在测试执行生命周期的各个时间点对测试进行扩展:



扩展开发者可以自由地实现所有或部分生命周期接口。

“BeforeAllCallback”接口提供了一种方法用于初始化扩展并在调用JUnit测试容器中的测试用例之前添加自定义逻辑。我们的StoryExtension类将实现这个接口,以确保给定的测试类使用了“@Story”注解。

import org.junit.jupiter.api.extension.BeforeAllCallback;

public class StoryExtension implements BeforeAllCallback {
 @Override
 public void beforeAll(ExtensionContext context) throws Exception {

  if (!AnnotationSupport
    .isAnnotated(context.getRequiredTestClass(), Story.class)) {
   throw new Exception(“Use @Story annotation...“);
  }
 }
}

Jupiter引擎将提供一个用于运行扩展的执行上下文。我们使用这个上下文来确定正在执行的测试类是否使用了“@Story”注解。我们使用JUnit平台提供的AnnotationSupport辅助类来检查是否存在这个注解。

回想一下,我们的自定义扩展在执行测试后会生成BDD报告。这些报告的某些部分是从“@Store”注解的元素中提取的。我们使用beforeAll回调来保存这些字符串。稍后,在执行生命周期结束时,再基于这些字符串生成报告。我们使用了一个简单的POJO。我们将这个类命名为“StoryDe​​tails”。以下代码片段演示了创建这个类实例的过程,并将注解元素保存到实例中:

public class StoryExtension implements BeforeAllCallback {
 @Override
 public void beforeAll(ExtensionContext context) throws Exception {

  Class<?> clazz = context.getRequiredTestClass();
  Story story = clazz.getAnnotation(Story.class);

  StoryDetails storyDetails = new StoryDetails()
    .setName(story.name())
    .setDescription(story.description())
    .setClassName(clazz.getName());

  context.getStore(NAMESPACE).put(clazz.getName(), storyDetails);
 }
}

我们需要解释一下方法的最后一个语句。我们实际上是从执行上下文中获取一个带有名字的存储,并将新创建的“StoryDe​​tails”实例保存到这个存储中。

自定义扩展可以使用存储来保存和获取任意数据——基本上就是一个存在于内存中的map。为了避免多个扩展之间出现意外的key冲突,JUnit引入了命名空间的概念。命名空间是一种对不同扩展保存的数据进行隔离的方法。用于隔离扩展数据的一种常用方法是使用自定义扩展类名:

private static final Namespace NAMESPACE = Namespace
   .create(StoryExtension.class);

我们的扩展需要用到的另一个自定义注解是“@Scenario”注解。这个注解用于将测试方法标记为故事中的场景或行为。我们的扩展将解析这些场景,以便将它们作为JUnit测试用例来执行并生成报告。回想一下我们之前看到的生命周期图中的“BeforeEachCallback”接口,在调用每个测试方法之前,我们将使用回调来添加附加逻辑:

import org.junit.jupiter.api.extension.BeforeEachCallback;

public class StoryExtension implements BeforeEachCallback {
 @Override
 public void beforeEach(ExtensionContext context) throws Exception {
  if (!AnnotationSupport.
   isAnnotated(context.getRequiredTestMethod(), Scenario.class)) {
    throw new Exception(“Use @Scenario annotation...“);
  }
 }
}

如前所述,Jupiter引擎将提供一个用于运行扩展的执行上下文。我们使用上下文来确定正在执行的测试方法是否使用了“@Scenario”注解。

回到本文的开头,我们提供了一个故事的示例代码,我们的自定义扩展负责将“Scene”类的实例注入到每个测试方法中。Scene类让测试用例编写者能够使用“given”、“then”和“when”等步骤来定义场景(行为)。Scene类是我们自定义扩展的中心单元,它包含了特定于测试方法的状态信息。状态信息可以在场景的各个步骤之间传递。我们使用“BeforeEachCallback”接口在调用测试方法之前准备一个Scene实例:如前所述,Jupiter引擎将提供一个用于运行扩展执行上下文。我们使用上下文来确定正在执行的测试方法是否使用了“@Scenario”注解。

public class StoryExtension implements BeforeEachCallback {
 @Override
 public void beforeEach(ExtensionContext context) throws Exception {
  Scene scene = new Scene()
    .setDescription(getValue(context, Scenario.class));

  Class<?> clazz = context.getRequiredTestClass();

  StoryDetails details = context.getStore(NAMESPACE)
    .get(clazz.getName(), StoryDetails.class);

  details.put(scene.getMethodName(), scene);
 }
}

上面的代码与我们在“BeforeAllCallback”接口方法中所做的非常相似。

动态参数解析

现在我们还缺少一个东西,即如何将场景实例注入到测试方法中。Jupiter的扩展模型为我们提供了一个“ParameterResolver”接口。这个接口为测试引擎提供了一种方法,用于识别希望在测试执行期间动态注入参数的扩展。我们需要实现这个接口的两个方法,以便注入我们的场景实例:

import org.junit.jupiter.api.extension.ParameterResolver;

public class StoryExtension implements ParameterResolver {
 @Override
 public boolean supportsParameter(ParameterContext parameterContext,
          ExtensionContext extensionContext) {
  Parameter parameter = parameterContext.getParameter();

  return Scene.class.equals(parameter.getType());
 }

 @Override
 public Object resolveParameter(ParameterContext parameterContext,
         ExtensionContext extensionContext) {
  Class<?> clazz = extensionContext.getRequiredTestClass();

  StoryDetails details = extensionContext.getStore(NAMESPACE)
    .get(clazz.getName(), StoryDetails.class);

  return details.get(extensionContext
       .getRequiredTestMethod().getName());
 }
}

上面的第一个方法告诉Jupiter我们的自定义扩展是否可以注入测试方法所需的参数。

在第二个方法“resolveParameter()”中,我们从执行上下文的存储中获取StoryDe​​tails实例,然后从StoryDetails实例中获取先前为给定测试方法创建的场景实例,并将其传给测试引擎。测试引擎将这个场景实例注入到测试方法中并执行测试。请注意,仅当“supportsParameter()”方法返回true值时才会调用“resolveParameter()”方法。

最后,为了在执行完所有故事和场景后生成报告,自定义扩展实现了“AfterAllCallback”接口:

import org.junit.jupiter.api.extension.AfterAllCallback;

public class StoryExtension implements AfterAllCallback {
 @Override
 public void afterAll(ExtensionContext context) throws Exception {

  new StoryWriter(getStoryDetails(context)).write();
 }
}

“StoryWriter”是一个自定义类,可生成报告并将其保存到JSON或文本文件中。

现在,让我们看看如何使用这个自定义扩展来编写BDD风格的测试用例。Gradle 4.6及更高版本支持使用JUnit 5运行单元测试。你可以使用build.gradle文件来配置JUnit 5。

dependencies {
 testCompile group: “ud.junit.bdd”, name: “bdd-junit”,
    version: “0.0.1-SNAPSHOT”

 testCompile group: “org.junit.jupiter”, name: “junit-jupiter-api”,
    version: “5.2.0"
 testRuntime group: “org.junit.jupiter”, name: “junit-jupiter-engine”,
    version: “5.2.0”
}

test {
 useJUnitPlatform()
}

如你所见,我们通过“useJUnitPlatform()”方法要求gradle使用JUnit 5。然后我们就可以使用StoryExtension类来编写测试用例。这是本文开头给出的示例:

import org.junit.jupiter.api.extension.ExtendWith;

import ud.junit.bdd.ext.Scenario;
import ud.junit.bdd.ext.Story;
import ud.junit.bdd.ext.StoryExtension; 

@ExtendWith(StoryExtension.class)
@Story(name=“Returns go back to the stockpile”, description=“...“)
public class StoreFrontTest {

 @Scenario(“Refunded items should be returned to the stockpile”)
 public void refundedItemsShouldBeRestocked(Scene scene) {
  scene
   .given(“customer bought a blue sweater”,
      () -> buySweater(scene, “blue”))

   .and(“I have three blue sweaters in stock”,
      () -> assertEquals(3, sweaterCount(scene, “blue”),
        “Store should carry 3 blue sweaters”))

   .when(“the customer returns the blue sweater for a refund”,
      () -> refund(scene, 1, “blue”))

   .then(“I should have four blue sweaters in stock”,
      () -> assertEquals(4, sweaterCount(scene, “blue”),
        “Store should carry 4 blue sweaters”))
   .run();
 }
}

我们可以通过“gradle testClasses”来运行测试,或者使用其他支持JUnit 5的IDE。除了常规的测试报告外,自定义扩展还为所有测试类生成BDD文档。

结论

我们描述了JUnit 5扩展模型以及如何利用它来创建自定义扩展。我们设计并实现了一个自定义扩展,测试用例编写者可以使用它来创建和执行故事。读者可以从GitHub上获取代码,并研究如何使用Jupiter扩展模型及其API来实现自定义扩展。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 详解springboot中junit回滚

    springboot中使用junit编写单元测试,并且测试结果不影响数据库. pom引入依赖 如果是IDE生成的项目,该包已经默认引入. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency&g

  • SSM框架整合之junit测试的方法

    1:和junit一起使用的时候因为没有读取配置文件,所以老是报创建Bean失败,上网查了查,原来是先要读取spring的核心配置文件,这样机也能够启动IOC容器了, 可以先创建一个父类,在父类里面读取配置文件创建IOC容器,然后让子类继承他就可以了 BaseTest.java package com.carry.ssm.test; import javax.annotation.Resource; import javax.security.auth.PrivateCredentialPermi

  • JUnit5相关内容简介

    著名的Java单元测试框架Junit 4已经出来很长时间了,当时我发现JUnit 5已经处于测试版,就准备写文章来介绍JUnit 5.不过因为还是测试版,所以有些地方还不太完善,我也有点懒没有好好写.这几天突然想起这事了,在到官网上查看,发现就在9月10日,JUnit 5的正式版终于出来了!那么我就正好把文章重新好好写写,为大家介绍这个最新的JUnit框架. 框架结构 和JUnit 4相比,JUnit 5的结构非常清晰,为自定义插件.IDE测试执行等扩展功能做了很好的支持.这一点从项目结构就可以

  • Spring Boot 单元测试JUnit的实践

    一.介绍 JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于白盒测试和回归测试. <!--more--> 白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人 员是公开的: 回归测试:软件或环境修复或更正后的再测试: 单元测试:最小粒度的测试,以测试某个功能或代码块.一般由程序员来做,因为它需要知道内部程序设计和编码的细节: JUnit GitHub地址:ht

  • 基于Springboot+Junit+Mockito做单元测试的示例

    前言 这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取撮合交易的一个类来做单元测试. 单元测试前先理解需求 要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测.但本文主要讲mockito的用法,无需关注具体需求.所以本节略去具体的需求描述. 隔离外部依赖 Case1. 被测类中被@Autowired 或 @Resource 注解标注的依赖对象,如何控制其返回值 以被测方法 MatchingServiceImpl.java的matching(Ma

  • 详解Junit 测试之 Spring Test

    在做spring相关测试时比较麻烦,如果只用JUnit测试,需要没测有初始化一下applicationContext,效率比较底下,而且也有不足之处. 1.导致多次Spring容器初始化问题 根据JUnit测试方法的调用流程,每执行一个测试方法都会创建一个测试用例的实例并调用setUp()方法.由于一般情况下,我们在setUp()方法 中初始化Spring容器,这意味着如果测试用例有多少个测试方法,Spring容器就会被重复初始化多次.虽然初始化Spring容器的速度并不会太 慢,但由于可能会在

  • 解决java junit单元测试@Test报错的问题

    在我们在myeclips里使用junit测试工具时有时会遇到错误,这是什么原因呢? 导致问题的原因通常有下面几个: (1)没有导入jar包 (2)导入jar包版本太低 (3)注意@Test要写在方法上面 如果不是几种问题,那便试试下面的解决方案: 1.在项目上点击右键,出现下图内容,选择properties 2.出现如下对话框,点击java build path,再选择add Library 3.之后如下图操作 4.选择junit4,点击finish,配置完毕. 以上这篇解决java junit

  • java编程之单元测试(Junit)实例分析(附实例源码)

    本文实例讲述了java编程之单元测试.分享给大家供大家参考,具体如下: 完整实例代码代码点击此处本站下载. 在有些时候,我们需要对我们自己编写的代码进行单元测试(好处是,减少后期维护的精力和费用),这是一些最基本的模块测试.当然,在进行单元测试的同时也必然得清楚我们测试的代码的内部逻辑实现,这样在测试的时候才能清楚地将我们希望代码逻辑实现得到的结果和测试实际得到的结果进行验证对比. 废话少说,上代码: 首先创建一个java工程,在工程中创建一个被单元测试的Student数据类,如下: packa

  • Junit 5中@ParameterizedTest与@EnumSource结合使用

    概述 有时候业务代码里会根据一个枚举值来区分业务场景,比如说: public enum ActivityLimitEnum { LIMIT(1,"封顶"), UNLIMIT(0,"上不封顶"); } 如果编写单元测试来验证业务代码,至少需要写两个测试方法,一个验收封顶逻辑,一个验收不封顶逻辑. @Test @DisplayName("封顶") void testLimit() { } @Test @DisplayName("不封顶&qu

  • 详解Spring Boot Junit单元测试

    Junit这种老技术,现在又拿出来说,不为别的,某种程度上来说,更是为了要说明它在项目中的重要性. 凭本人的感觉和经验来说,在项目中完全按标准都写Junit用例覆盖大部分业务代码的,应该不会超过一半. 刚好前段时间写了一些关于SpringBoot的帖子,正好现在把Junit再拿出来从几个方面再说一下,也算是给一些新手参考了. 那么先简单说一下为什么要写测试用例 1. 可以避免测试点的遗漏,为了更好的进行测试,可以提高测试效率 2. 可以自动测试,可以在项目打包前进行测试校验 3. 可以及时发现因

随机推荐