写点什么

深入理解 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:083517
用户头像

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

关注

评论

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

浅谈LocalCache | 京东云技术团队

京东科技开发者

OurBMC社区首场Meetup成功举办,共建BMC产业生态

OurBMC

Meetup 汇聚智力 共建BMC

“祥龙守神州,舞瑞中国年”,京东超市携手王牌驼喜迎新春

科技热闻

IT工单治理野史:由每周最高150+治理到20+ | 京东物流技术团队

京东科技开发者

Kubeadmiral 开源编程挑战 —— 我觉得不错

miraclejzd

字节跳动 Kubernetes 云原生 Kubeadmiral

OurBMC技术委员会2023年四季度例会顺利召开

OurBMC

技术委员会 工作汇报 四季度例会

淘宝/天猫商品详情API:返回值参数详解及商业逻辑实现

Noah

【教程】Python代码混淆工具,Python源代码保密、加密、混淆

雪奈椰子

假期想学习,送你测试开发+人工智能大礼包

霍格沃兹测试开发学社

部署Palworld幻兽帕鲁服务器最佳实践(Ubuntu)

天翼云开发者社区

云计算 最佳实践 服务器 云服务器

有了ERP和MES,还需要质量管理QMS系统吗?

万界星空科技

数字化 生产管理系统 mes 万界星空科技 QMS

OurBMC 社区 SIG 建设月报(2023 年 10 月)

OurBMC

SIG月报 SIG进展

使用SD-WAN进行企业网络升级的必要性

Ogcloud

SD-WAN SD-WAN组网 SD-WAN服务商

OpenSPG新版发布:大模型知识抽取与快速知识图谱构建

百度开发者中心

人工智能 知识图谱 智能客服 大模型

Palworld幻兽帕鲁世界参数修改最佳实践(Ubuntu)

天翼云开发者社区

云计算 最佳实践 云服务器

Wireshark中的http协议包分析

小齐写代码

大文件上传原理及实现方案 | 京东物流技术团队

京东科技开发者

通义灵码——灵动指间,快码加编,你的智能编码助手

阿里巴巴云原生

阿里云 云原生

迎龙年接新春,来华为手机里寻找祥龙

最新动态

自动化测试,有最佳实践吗?

老张

软件测试 自动化测试

OurBMC大咖说 | OurBMC,共创国产软硬件开源发展新纪元

OurBMC

大咖说 软硬件开源 BMC技术全栈

OurBMC运营委员会2023年下半年度例会顺利召开

OurBMC

运营委员会 工作汇报 首次例会

探索大模型训练与多模态数据处理

百度开发者中心

人工智能 图像 大模型训练

测试开发+人工智能大礼包,让你在假期实现弯道超车

测试人

软件测试

这篇深入浅出贴 助你早日实现Stable diffusion自由

京东科技开发者

全新 Amazon S3 Express One Zone 高性能存储类服务,震撼发布!

亚马逊云科技 (Amazon Web Services)

Gas Hero Coupon NFT 概览与数据分析

Footprint Analytics

区块链 加密货币 NFT

Seal 新春大挑战等你来参与!

SEAL安全

AI DevOps Walrus

【教程】一个比较良心的C++代码混淆器

玩转OurBMC第一期:社区操作指南-功能篇

OurBMC

玩转OurBMC 操作指南 基本功能

100%中奖、会员回馈礼…星河会员新春福利到!

飞桨PaddlePaddle

百度 飞桨 飞桨AI 飞桨星河社区

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