快手、孩子王、华为等专家分享大模型在电商运营、母婴消费、翻译等行业场景的实际应用 了解详情
写点什么

JUnit 5 – 早期试用体验 – 第 2 篇

  • 2016-10-23
  • 本文字数:6628 字

    阅读完需:约 22 分钟

主要结论

  • JUnit 5 就要来了!
  • 其中包含改进的 API 和扩展模型将大幅完善“JUnit 工具”。
  • 模块化的体系结构使得“JUnit 平台”可以用于其他测试框架。
  • 虽然经过了彻底重写,但可在同一个代码基中与老版本 Junit 共存。

JUnit5 第一篇覆盖的范围内,我们看了如何设置 JUnit 和开始编写测试,以及看到了表面上发生的变化。我们也讨论了重写的必要性以及新架构是如何划分出 Junit Platform 和 JUnit Jupiter 的。

在这第二篇,我们将会仔细的看看如何运行测试和 JUnit5 带给我们开发者的一些非常酷的新特性。

运行测试

JUnit Jupiter 测试能够作为 JUnit4 的一部分运行或者跑在 JUnit5 基础设施上。

作为 JUnit4 的一部分

还记得那 5 秒的设置么?使用下面的内容:

复制代码
org.junit.jupiter:junit-jupiter-api
org.junit.jupiter:junit-jupiter-engine
org.junit.platform:junit-platform-runner

正如我们第一篇所说的那样,我们需要 junit-jupiter-api 去编写我们的单元测试,需要 junit-jupiter-engine 去发现和运行它们。最后那个,junit-platform-runner 包含 JUnit4 Runner 接口的实现类 JUnitPlatform,该类仅是简单地通知引擎运行测试。使用 @RunWith(JUnitPlatform.class) 标注以 JUnit4 的一部分运行我们的测试。需要注意的是,这样能够正常工作的类必须是常规的 JUnit4 测试类。例如,它必须符合你选择的工具的约定(像 Maven 的命名约定)。使用这种方式,JUnit Jupiter 测试将能够跑在任何集成了 JUnit4 的工具中。

然而这里我们能够做小的改进。执行器能够识别 @SelectPackages 注解,从而用来运行某个包下的所有测试。

复制代码
package com.infoq.junit5;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.runner.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages({ "com.infoq.junit5" })
public class TestWithJUnit5 { }

在这里,包被解析成一种层次结构,该结构表示所有以 com.infoq.junit5 开头的包中的测试都将被运行。因为这些类都是被 JUnit Platform 运行器自动发现的 (替代了 JUnit4 集成工具),所以这些类都不再必须是 public 的。

警告:如果我们使用了利用该新架构的机制(我们马上就会讨论这个)并且该机制包含 JUnit4 的引擎,这些测试将会被执行两次:一次在 JUnit4 运行的时候(由于我们应用了 @RunWith 的执行器),另外一次在 JUnit5 运行的时候。

使用 JUnit5

现在,让我们看看如何让全套 JUnit5 机制运行起来。我们可以使用 junit-platform-launcher 提供的 API,然后绑定若干个引擎 (对于现在可能是 JUnit4 和 5) 去发现和运行测试。

集成开发环境

IntelliJ IDEA 自从 2016.2 版本就有了基础的 JUnit5 支持。虽然这还不完美,因为这相当于在追逐一个移动的目标,但是这就使新的 JUnit 更容易使用了。

Eclipse 团队也正在为原生支持而努力工作,因此估计时间不会太久。

构建工具支持

JUnit 团队本身已经在构建工具支持上努力工作;初步的 Gradle 插件和 Maven Surefire provider 已经投入使用,一旦社区准备好接受它们,这两个项目都计划移交给各自的社区。

Gradle Maven 都有示例项目。关于更多的细节可以查看 Junit5 用户指南

命令行必胜!

如果你不喜欢 IDE 和构建工具,你可以尝试控制台启动,该功能允许你可以直接在命令行启动测试。要得到该功能你需要下载压缩包。使用该功能最方便的方式是将 junit-jupiter-api 和 junit-jupiter-engine 内容放入 lib 目录,然后编辑 class path 的定义脚本,定义 CLASSPATH=$APP_HOME/lib/*。

你可以如下这么使用:

复制代码
# run all tests
junit-platform-console -p ${path_to_compiled_test_classes} -a
# run a specific test
junit-platform-console
-p ${path_to_compiled_test_classes}
org.infoq.junit5.HelloWorldTest

如果你还有其他依赖,例如 Mockito 之类的其他测试库,把它们加入到 class path,在 -p 后面列出它们。

闪亮的新特性

我们已经了解了 JUnit 的新架构,如何基于现有的工具支持去设置它。相比于过去,这将改善我们编写测试的 API。接下去,让我们转而去看它带给我们的新特性。

嵌套测试

JUnit Jupiter 使得编写嵌套测试类完全不费力,意味着你可以用 BDD(行为驱动开发)的风格组织测试类。你只需要在内部类上加注解 @Nested。

复制代码
class NestedTest {
int count = Integer.MIN_VALUE;
@BeforeEach
void setCountToZero() {
count = 0;
}
@Test
void countIsZero() {
assertEquals(0, count);
}
@Nested
class CountGreaterZero {
@BeforeEach
void increaseCount() {
count++;
}
@Test
void countIsGreaterZero() {
assertTrue(count > 0);
}
@Nested
class CountMuchGreaterZero {
@BeforeEach
void increaseCount() {
count += Integer.MAX_VALUE / 2;
}
@Test
void countIsLarge() {
assertTrue(count > Integer.MAX_VALUE / 2);
}
}
}
}

生命周期的方法 @BeforeEach 和 @AfterEach 在这里也能工作,按照由外到内的顺序执行。这样就可以增量构建用于内部测试的上下文了。

为了充分利用该设置,重点在于内部类必须拥有访问外部测试类字段的权限。这就要求内部类必须不是静态的。因此静态方法是禁止使用的,所以 @BeforeAll 和 @AfterAll 在这种情况下就无法使用了。

在我们介绍完另一个新特性后,我们将会看到嵌套的测试结果是如何显示的,该特性和 @Nested 可以很好地配合。

命名测试

开发人员经常让测试的名称能够表达测试的前置条件、被测试的单元甚至是预期的行为。在一个方法名中满足这些需求会令它变得非常笨拙。

JUnit 带来了这个问题的一种解决方案。新的注解 @DisplayName 接受任意的字符串,被 JUnit 用来作为类或者方法的显示名。JUnit 团队经常给出以下示例:

复制代码
@DisplayName("A stack")
class TestingAStack {
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() { /*...*/ }
@Nested
@DisplayName("when new")
class WhenNew {
@Test
@DisplayName("is empty")
void isEmpty() { /*...*/ }
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() { /*...*/ }
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() { /*...*/ }
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
@Test
@DisplayName("it is no longer empty")
void isEmpty() { /*...*/ }
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() { /*...*/ }
@Test
@DisplayName(
"returns the element when peeked but remains not empty")
void returnElementWhenPeeked(){ /*...*/ }
}
}

组合使用 @Nested 和 @DisplayName 可以创造出易读的输出,为从事行为驱动开发的人带来快乐。

参数注入

在以前,测试方法不允许带参数。那是有道理的,毕竟 JUnit 可能传递什么给它们呢?在版本 5 中,团队回答了这个问题就是“任何你想传的!”。

因此,现在测试方法可以有参数了。对于每一个参数,JUnit 将会搜索一个扩展来提供值。有两个这样的扩展是内置的,能够被用来注入 TestInfo 和 TestReporter,但这两个扩展在日常测试编写中并不常用。

更有趣的是 MockitoExtension,该扩展会注入一个 mock 到所有以 @InjecMock 注解的参数。这显示了虽然扩展 API 仍然在开发中,但是已经能够被善加利用了。

因此,让我们了解一下吧。

可扩展性

JUnit Lanmda 项目有几个核心原则,其中一个就是“扩展点优于特性”。这是个伟大的原则,JUnit5 看上去将会很好的实现该原则。

自定义注解

所有的 JUnit 注解都能够被当作元注解使用。也就是说,它们可以用来标注其他注解。Jupiter 引擎预料到了这种情况,能够接受这些元注解就像它们直接标注在对应的元素上一样。

有了这个,我们很轻松就能创建出被 JUnit Jupiter 完全支持的自定义注解。

复制代码
/**
* We define a custom annotation @IntegrationTest that:
* - stands in for '@Test' so that the method gets executed
* - has the tag "integration" so we can filter by that,
* e.g. when running tests from the command line
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("integration")
public @interface IntegrationTest { }

我们可以这样使用:

复制代码
@IntegrationTest
void runsWithCustomAnnotation() {
// this is run even though `@IntegrationTest` is not defined by JUnit
}

真棒!一个简单而贴心的特性,将对扩展产生巨大的影响。

扩展点

JUnit 定义了能够用于注入行为的特定扩展点。让我们看看当前已经定义的扩展点:

TestInstancePostProcessor

能够执行条件计算,该结果决定是否执行测试。举个例子,比如只有在指定的操作系统或者外部的资源有效的时候才运行测试。

BeforeAll, BeforeEach, AfterEach, AfterAll

在测试执行前或者执行后立即执行。

ParameterResolver

该扩展点在测试抛出异常结束测试的时候被执行。它可以用来在出错的时候回滚数据库事务或者吞掉预期的异常。

这些扩展点都有一个接口。每个接口都定义了方法,这些方法会在适当的时候被 Jupiter 引擎调用,同时传入相应的上下文到扩展点(比如测试实例,测试方法,参数,当前的注解等)。

为了实际的应用某个扩展,测试类或者方法必须加上注解 @ExtendWieh(OurNewExtension.class)。

让我们看两个例子亲身感受下这个是如何工作的。如果有兴趣了解更多,参看 Rüdiger Herrmann 的文章《如何替换JUnit5 的规则》,并自己进行实验。

条件

@Disabled

我们已经看到使用 @Disable 注解很简单就能把测试禁用了。让我们看看这是怎么实现的。

DisabledCondition 类实现了接口 TestExecutionCondition 和 ContainerExecutionCondition。相应的方法被调用的时候带有上下文,该上下文能够用来检查 @Disabled 注解是否存在。如果存在,方法就会返回一个值表明该测试被禁用。

见代码:

复制代码
@Override
public ConditionEvaluationResult evaluate(
ContainerExtensionContext context) {
return evaluate(context.getElement());
}
@Override
public ConditionEvaluationResult evaluate(
TestExtensionContext context) {
return evaluate(context.getElement());
}
private ConditionEvaluationResult evaluate(
Optional<AnnotatedElement> element) {
Optional<Disabled> disabled =
findAnnotation(element, Disabled.class);
if (disabled.isPresent()) {
String reason = /* … */;
return ConditionEvaluationResult.disabled(reason);
}
return ENABLED;
}

现在我们知道 DisabledCondition 扩展类负责实际实现 @Disabled 想要的行为。那么为什么这里我们不使用 @ExtendWith(DisabledCondition.class) 来禁用测试?

在该注解外,还有一个扩展注册中心,那里包含内置的扩展来减少开销。尽管还有另外一种,稍微有一点点迂回的方式来做这个事情。我们现在就用这个来实现我们自己的条件注解。

@Available

让我们假设有一个集成测试,它依赖于 REST 服务当前是否有效。当远程终结点挂掉的时候需要禁用这些测试。我们创建注解 @Available,接收一个字符串作为值,这样我们就可以按照下面的方式使用:

复制代码
@Test
@Available(“https://www.appdynamics.com”)
void testAuthorSearch() {
// the test
}

接着,我们创建名为 AvailableCondition 的类,该类和上面的很像。该类实现和上面一样的接口(因此我们能够在测试类和独立的方法上使用该注解)并将两个方法调用传递给私有的 evaluate 方法,该方法这里只需要已注解的元素。

如果注解存在,它会取出 URL 里面的值然后做一次测试调用。只有当调用符合要求的时候对应的测试才会被执行。

现在,我们需要做的就是通知 JUnit,让它知道 AvailableCondition 这个类。我们可以在所有的这类测试上加上注解 @ExtendWith(AvailableCondition.class),当然这样做太没意思了。现在让我们来看一下我提到过的小技巧。还记得元注解以及 JUnit 如何寻找它们么?我们能够在这里使用并给注解 @Available 加上我们的扩展。JUnit 将会发现并且立即应用它:

复制代码
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(AvailableCondition.class)
public @interface Available {
String value();
}

我喜欢这个。

注入

让我们回到上文提到过的 MockitoExtension ,然后看看它是如何工作的。它实现了 ParameterResolver 接口。该接口包含两个方法定义:

  • 第一个是 supports,用来判断 resolver 是否支持对应的参数。
  • 第二个是 resolve,需要返回将会注入的实例。

我们只注入带有注解的参数,这看上去很合理。因此我们这样实现:

复制代码
@Override
public boolean supports({1}
ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext
.getParameter()
.isAnnotationPresent(InjectMock.class);
}

使用 Mockito 创建 Mock 易如反掌:

复制代码
@Override
public Object resolve({1}
ParameterContext parameterContext,
ExtensionContext extensionContext) {
Class<?> parameterType = parameterContext().getParameter().getType();
return Mockito.mock(parameterType);
}

(实际的实现会略微复杂点,因为它允许在生命周期的方法中设置 mock,这意味着它必须跟踪 mock 实例而不是总创建新的实例。但是这是最基本的模式。)

我们能够使用类似的手段,用来注入配置服务或者其他有意义的对象到我们的测试里。

总结

到此,我们关于 JUnit5 的深度了解就要结束了,让我们慢慢地重新回顾一下。

我们已经知道 JUnit4 的扩展机制存在问题,它本身也缺乏模块化,需要完全的重写才能继续向前发展。在 2015 年,一个由经验丰富的开发人员组成的团队收集并整理了这些想法,由他们的老板发起,一大群人投入了几个月就开发出了原型(即 alpha 版),以及最近的里程碑 1 和 2。

JUnit 的架构分化出“JUnit 平台”和“JUnit 工具”两个部分。JUnit Platform就是前者的实现,这是一个用来组织各种不同引擎的启动器。目前,已经有引擎为 JUnit4(JUnit Vintage)和 JUnit5 做了实现。不过在将来,所有测试框架都可以提供它们自己的引擎。每个引擎都可以运行基于它们自己特定 API 的测试。架构的另外一个部分是JUnit Jupiter,也就是“JUnit 工具”。该工具是开发人员用来编写测试的,它由只包含 API 的 JAR 包组成,非常的精简。

JUnit4 用来实现扩展性的手段是运行器和规则,这些将会被扩展点代替。扩展点存在于 JUnit Jupiter 生命周期的各个阶段中,从测试实例的创建到条件执行,以及异常处理。

在巨大改变的新架构和扩展模型之上的是闪亮的新特性层:测试能够被简单的命名和嵌套,并能被注入参数。再上面的层,变化就比较小了。Jupiter 由可见级别为同个包内的类和方法组成,只是稍微重命名了生命周期注解以及增量改进了断言和假说。

一瞥之下甚至看不出这两个版本的区别。

那么接下来会发生什么?我们可以基于我们的代码去查看里程碑 2,去思考那些可能不太容易表达的方面。这些案例是 JUnit 团队特别感兴趣的!

他们按顺序,继续改进项目。最近他们刚完成的特性是动态测试生成,该功能允许运行时创建测试,可以解锁备受期待的lambda 测试功能。计划上是再发布另外一个里程碑,甚至可能在今年年底发布final 版本。

我已经等不及了!

Nicolai Parlog是一位软件开发者兼 Java 传教士。他会经常阅读、思考并撰写有关 Java 的文章,在以写代码为生的同时也享受着写代码的乐趣。他是多个开源项目的长期贡献者,并维护了一个有关软件开发的博客: CodeFX 。你也可以在 Twitter 关注 Nicolai。

查看英文原文: JUnit 5 - An Early Test Drive - Part 2


感谢冬雨对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-10-23 17:564006

评论

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

Kafka 中的消息存储在磁盘上的目录布局是怎样的?

码农架构

Java kafka 架构 设计模式

Android经典面试:46道面试题带你了解中高级Android面试,附面试题答案

欢喜学安卓

android 程序员 面试 移动开发

如何 3 步一键部署开源容器应用?

binggg

Docker 开源 Serverless 云开发 应用

Apache Flink 在实时金融数据湖的应用

Apache Flink

flink

大数据知识专栏 -MapReduce 自定义计数器技术

小马哥

大数据 mapreduce 七日更

volatile,还可以有这么硬的理解

Java 程序员 线程

架构师训练营第三周作业 -命题作业

阿德儿

15个国内外最受欢迎的YouTube视频下载器

科技猫

youtube视频下载 油管视频下载 下载youtube视频 下载油管视频 视频下载器

合约交易APP系统开发|合约交易软件开发

系统开发

备忘录1

Vei

2021最新版阿里巴巴Java性能调优速成手册强烈推荐

比伯

Java 编程 架构 面试 架构师

初步解析 Elasticsearch Document 核心元数据

escray

elastic 七日更 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

长文攻略|如何打造一键部署的云开发应用

binggg

小程序 大前端 全栈 开发应用 云开发

【CSS】画三角形(8个角度及其原理)

德育处主任

CSS html5 大前端 CSS小技巧 28天写作

比特币矿机工作原理

v16629866266

基于 KubeEdge 和 Kuiper 的边缘流式数据处理实践

华为云原生团队

数据库 云原生 边缘计算 华为云 边缘技术

android开发培训!深度解析跳槽从开始到结束完整流程,系列篇

欢喜学安卓

android 程序员 面试 移动开发

对容器镜像的思考和讨论

阿里巴巴云原生

Docker 容器 开发者 云原生 CloudNative

即构✖叮咚课堂:行业第一套AI课堂解决方案是怎么被实现的?

ZEGO即构

新“庖丁解牛”,华为云技术全牛图解

陈泽涛

Hadoop编程实战:HDFS用户Shell详解

罗小龙

hadoop 最佳实践 28天写作 hdfs shell

Web UI自动化测试之元素定位

行者AI

软件测试 测试 自动化测试

【Redis】- Redis Cluser之数据分布

双木之林

吉他谱怎么看?看谱大攻略送上!

懒得勤快

音乐 吉他学习 吉他谱 看谱

2020年中国DevOps应用发展研究——艾瑞咨询报告总结

禅道项目管理

DevOps 行业资讯 趋势

简单五步:利用Gitstats给代码仓库做一次体检

后台技术汇

28天写作

图解分布式之:最终一致性,一致只会迟到,但绝不缺席

四猿外

架构 分布式 分布式系统 一致性 数据一致性

区块链数字货币交易所系统软件APP开发

系统开发

【Java虚拟机】- Java虚拟机之逃逸分析

双木之林

迟到的年度总结-数据的人生

松子(李博源)

大数据 数据中台 总结 年度总结

AQS之ReentrantReadWriteLock精讲分析上篇

伯阳

AQS 读写锁 ReentrantReadWriteLock 多线程与高并发 lock

JUnit 5 – 早期试用体验 – 第2篇_Java_Nicolai Parlog_InfoQ精选文章