深入理解JUnit 5的扩展模型
\
关键要点
\\
- JUnit 5是一个模块化和可扩展的测试框架,支持Java 8及更高版本。\\t
- JUnit 5由三个部分组成——一个基础平台、一个新的编程和扩展模型Jupiter,以及一个名为Vintage的向后兼容的测试引擎。\\t
- JUnit 5 Jupiter的扩展模型可用于向JUnit中添加自定义功能。\\t
- 扩展模型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”,\ () -\u0026gt; buySweater(scene, “blue”))\ \ .and(“I have three blue sweaters in stock”,\ () -\u0026gt; assertEquals(3, sweaterCount(scene, “blue”),\ “Store should carry 3 blue sweaters”))\ \ .when(“the customer returns the blue sweater for a refund”,\ () -\u0026gt; refund(scene, 1, “blue”))\ \ .then(“I should have four blue sweaters in stock”,\ () -\u0026gt; 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能够借助以下核心概念来支持和执行故事和行为:
\\
- 用于装饰测试类和测试方法的注解\\t
- JUnit 5 Jupiter的生命周期回调\\t
- 动态参数解析\
注解
\\
示例中的“@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。我们将这个类命名为“StoryDetails”。以下代码片段演示了创建这个类实例的过程,并将注解元素保存到实例中:
\\
\public class StoryExtension implements BeforeAllCallback {\ @Override\ public void beforeAll(ExtensionContext context) throws Exception {\ \ Class\u0026lt;?\u0026gt; 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);\ }\}
\\
我们需要解释一下方法的最后一个语句。我们实际上是从执行上下文中获取一个带有名字的存储,并将新创建的“StoryDetails”实例保存到这个存储中。
\\
自定义扩展可以使用存储来保存和获取任意数据——基本上就是一个存在于内存中的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\u0026lt;?\u0026gt; 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\u0026lt;?\u0026gt; clazz = extensionContext.getRequiredTestClass();\ \ StoryDetails details = extensionContext.getStore(NAMESPACE)\ .get(clazz.getName(), StoryDetails.class);\ \ return details.get(extensionContext\ .getRequiredTestMethod().getName());\ }\}
\\
上面的第一个方法告诉Jupiter我们的自定义扩展是否可以注入测试方法所需的参数。
\\
在第二个方法“resolveParameter()”中,我们从执行上下文的存储中获取StoryDetails实例,然后从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”,\ () -\u0026gt; buySweater(scene, “blue”))\ \ .and(“I have three blue sweaters in stock”,\ () -\u0026gt; assertEquals(3, sweaterCount(scene, “blue”),\ “Store should carry 3 blue sweaters”))\ \ .when(“the customer returns the blue sweater for a refund”,\ () -\u0026gt; refund(scene, 1, “blue”))\ \ .then(“I should have four blue sweaters in stock”,\ () -\u0026gt; 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
深入理解JUnit 5的扩展模型相关推荐
- 扩展 junit 框架_JUnit 5 –扩展模型
扩展 junit 框架 我们已经对Java最普遍的测试框架的下一个版本了解很多. 现在,让我们看一下JUnit 5扩展模型,该模型将允许库和框架将自己的实现添加到JUnit中. 总览 建立 基本 建筑 ...
- JUnit 5 –扩展模型
我们已经对Java最普遍的测试框架的下一个版本了解很多. 现在让我们看一下JUnit 5扩展模型,该模型将允许库和框架将自己的实现添加到JUnit中. 总览 设定 基本 建筑 扩展模型 条件 注射 - ...
- 【多线程】0.理解一下5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO
5种IO模型.阻塞IO和非阻塞IO.同步IO和异步IO 看了一些文章,发现有很多不同的理解,可能是因为大家入切的角度.环境不一样.所以,我们先说明基本的IO操作及环境. 本文是在<UNIX网络编 ...
- BERT-QE:用于文档Rerank的上下文化查询扩展模型
BERT-QE 论文名称:EMNLP2020 | BERT-QE: Contextualized Query Expansion for Document Re-ranking arxiv地址:htt ...
- 循序渐进:带你理解什么是Java内存模型
近期笔者在阅读<深入理解Java虚拟机:JVM高级特性与最佳实现(第3版)>,书中提到关于Java内存模型的知识点,但是看完之后还是感觉有些模糊,便查阅一些其他相关资料.本文是笔者经过对知 ...
- SIGIR阿里论文 | 可视化理解深度神经网络CTR预估模型
小叽导读:尽管业界对于图像处理和自然语言处理领域,在算法可解释性方向上已经取得了一些进展,但对于电商与广告领域,目前还是空白.另一方面,深度学习技术已经开始被大规模应用到广告业务中.广告是很多互联网现 ...
- 信息抽取(一)机器阅读理解——样本数据处理与Baseline模型搭建训练(2020语言与智能技术竞赛)
机器阅读理解--样本数据处理与Baseline模型搭建训练 前言 样本数据处理 数据测试 模型部分 模型构建 模型训练 部分推理结果 总结 前言 最近看到今年早些时候百度的"2020语言与智 ...
- 中台背景下的多端自适应的业务扩展模型架构实践
前言 随着数字化变革的持续深入和中台战略的落地,越来来越多的最佳实践涌现出来,这里我不在赘述什么是数字化转型和什么是中台,这里我分享一下我们在中台建设中的一些顶层思考和实践.在中台规划之前中我一直在思 ...
- 扩展 junit 框架_JUnit 5扩展模型的生命周期
扩展 junit 框架 JUnit5最终版本即将来临 (当前是M4),我已经开始尝试如何编写扩展了. 在JUnit5中 ,您没有使用Runners , Rules , ClassRules等,而是只有 ...
- JUnit 5扩展模型的生命周期
JUnit5最终版本即将来临 (当前是M4),我已经开始研究如何编写扩展. 在JUnit5中 ,您没有使用Runners , Rules , ClassRules等,而是只有一个Extension A ...
最新文章
- Entity Framework中的Migration问题
- 做 AI 大咖在顶级单位之间随兴漂移,好开心!
- 某云数据中心网络解决方案(分享二十一)
- tq2440实验手册qt编译问题
- 实时数仓入门训练营:实时计算 Flink 版 SQL 实践
- python中形参*args和**kwargs简述
- java 中文 编译_java编译带中文是显示乱码的错误
- nginx 代理多个服务器——多个server方式
- 火绒规则 禁止所有软件的安装_十大机械设计软件对比,附所有软件安装资料...
- 8.10 数据库安全性II Day28
- golang学习的点点滴滴:if、switch使用
- CentOS7 安装aria2
- 全球及中国应用商店优化ASO工具行业竞争现状与发展战略规划研究报告2022-2028年
- java多线程技术体系
- p图软件pⅰc_P图教程|教你做超火的iMessage图 所需软件:Picsart QQ_修图软件_滤镜_picsart怎么样_纯白色_相册_我超会p图der_摄影_摄影技巧_修图技巧...
- JAVA生成安卓签名证书
- 电影与幸福感期末答案和平时测试答案
- 大型系统存储层迁移实践
- python基础知识之整除、取余、幂运算
- swift 百度地图加载与百度地图电子围栏加载
热门文章
- python之路_kindEditor编辑器及beautifulsoup模块
- 【分布计算环境学习笔记】2 分布式系统中的面向对象技术
- Dynamics Axapta的B/S解决之道(一)
- Mac资讯:macos big sur正式版推送 macOS 11 Big Sur有哪些不兼容的软件?
- k8s学习: ErrImagePull 和 ImagePullBackOff 怎么办?
- OC 中property属性详解(assign , retain , copy , strong,weak,readonly , readwrite , atomic , nonatomic)
- 如何利用FL Studio进行听湿录干的声音录制
- 简单好用一键恢复丢失办公文档
- Maven构建springBoot Demo案例
- Python- 索引 B+数 比如书的目录