红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

深入理解 JUnit 5 的扩展模型

  • 2018-08-27
  • 本文字数:11511 字

    阅读完需:约 38 分钟

关键要点

  • 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 能够借助以下核心概念来支持和执行故事和行为:

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

注解

示例中的“@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 来实现自定义扩展。

关于作者

Uday Tatiraju 是甲骨文的首席工程师,在电子商务平台、搜索引擎、后端系统以及 Web 和移动编程方面拥有超过十年的经验。

查看英文原文 Deep Dive into JUnit 5 Extension Model

2018-08-27 18:083209
用户头像

发布了 731 篇内容, 共 432.0 次阅读, 收获喜欢 1996 次。

关注

评论

发布
暂无评论
发现更多内容

数据库中间件MyCat实战笔记(第一步),arm架构linux系统

Java 程序员 后端

本科毕业斩获字节offer的我做了什么准备?大厂面试经验试题分享

Java 程序员 后端

来自北京大学NOIP金牌选手yxc的常用代码模板3——搜索与图论

Java 程序员 后端

数据库索引的原理,springcloud视频百度云

Java 程序员 后端

无论你是什么职业,这篇职场生存法则都是你必备的,java微服务架构技术

Java 程序员 后端

最新版SpringBoot开发实战:阿里技术官从基础到项目综合实战pdf

Java 程序员 后端

月薪60k的Java开发在阿里是什么级别?对技术能力有哪些要求?

Java 程序员 后端

最新基准测试:Kafka、Pulsar-和-RabbitMQ-哪个最快,阿里Java笔试题目

Java 程序员 后端

服了!阿里资深架构师发布SpringCloud笔记,在GitHub标星已达81

Java 程序员 后端

来自北京大学NOIP金牌选手yxc的常用代码模板4——数学知识

Java 程序员 后端

极速体验docker容器健康,高并发分布式系统架构

Java 程序员 后端

提升开发效率N倍的20+命令行神器,赶紧收藏了,mybatis原理图

Java 程序员 后端

数论 - 约数基础 【 试除法求所有约数 + 约数个数和约数之和

Java 程序员 后端

是什么Java面试题PDF被Git全面封杀?刷完这套题已经拿完9个Offer!

Java 程序员 后端

普通本科,毕业六年,复盘一个月,mybatis一级缓存和二级缓存面试题

Java 程序员 后端

普通程序员就不能有拥有架构师光环?想要建立架构思维,这份京东架构技术精髓一定不能错过

Java 程序员 后端

Go channel,面试官会这样问

Rayjun

channel Go 语言

数据源的概念是什么?Springcloud+Mybatis如何使用多数据源

Java 程序员 后端

斯坦福高效睡眠法-读书笔记,这可能是目前最全的

Java 程序员 后端

本科毕业斩获字节offer的我做了什么准备?大厂面试经验试题分享(1)

Java 程序员 后端

数据库ACID四大特性到底为了啥,一文带你看通透,java支付宝支付接口教程

Java 程序员 后端

架构师成长之路-docker 搭建es集群,rocketmq教程教程

Java 程序员 后端

最近被安排搞搜索接口优化,压测了4次,才勉强达到要求

Java 程序员 后端

有个厉害的程序员老婆是什么体验?,mysql性能优化面试题

Java 程序员 后端

如何用内网渗透突破安全策略

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

架构设计基础知识整理,java常用设计模式面试

Java 程序员 后端

数据结构的栈和队列(这不进来看一看),计算机java语言入门

Java 程序员 后端

数据结构系列第六部分:排序,Github爆火的《高并发秒杀顶级教程》

Java 程序员 后端

新人问一般都用哪些 Linux 命令,我把这个扔了过去,linux基础及应用教程课后答案

Java 程序员 后端

是什么让普通的链表也能达到二分查找的效率,你知道吗?

Java 程序员 后端

来自阿里大牛20000字总结-+-40张图文详解,我就不信你还参透不了并发编程

Java 程序员 后端

深入理解JUnit 5的扩展模型_Java_Uday Tatiraju_InfoQ精选文章