写点什么

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:564049

评论

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

2022年11月中国汽车智能网联月度观察

易观分析

汽车 智能网联

北京同仁堂两大名牌品种亮相帝都

联营汇聚

React 之 Context 的变迁与背后实现

冴羽

JavaScript 源码分析 前端 前端框架 React

Java培训一般需要多长时间?

小谷哥

Java开发技术很难吗?

小谷哥

“智造新未来”欧比护理智造总部奠基仪式

联营汇聚

云计算的六大核心技术,你了解多少?

Finovy Cloud

云技术 云渲染

Verilog 时延与过程结构

芯动大师

Verilog语法 Verilog延时 Verilog过程结构

前端培训学习前景怎么样

小谷哥

【前端相关】服务端渲染和客户端渲染的比较

No8g攻城狮

CSS css3 前端 js 前端框架

易观分析潘玉宇:信贷全流程化监管将成行业发展重点,银行间联合风控程度将逐渐加深

易观分析

银行 普惠金融

架构实战营 2-6 钱包高可用实战随堂练习

西山薄凉

「架构实战营」

时序数据库破局开放探讨

YMatrix 超融合数据库

物联网 时序数据库 超融合数据库 数据库架构选型 YMatrix

数据生态第四弹 | OpenMLDB Hive Connector,架构起数据仓库到特征工程的生态桥梁

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

FLStudio21.0.0水果官方中文版发布功能介绍

茶色酒

FLStudio21.0.0

带你实现react源码的核心功能

flyzz177

React

在成都培训web前端哪有比较好的机构

小谷哥

YMatrix:超融合数据库如何在泵车智能运维场景实现 One for All 价值

YMatrix 超融合数据库

智能运维 三一重工 超融合数据库 智能化运维 YMatrix

重磅 | 九科信息入选创新型中小企业(原深圳市专精特新企业)

九科Ninetech

2022-12-12:有n个城市,城市从0到n-1进行编号。小美最初住在k号城市中 在接下来的m天里,小美每天会收到一个任务 她可以选择完成当天的任务或者放弃该任务 第i天的任务需要在ci号城市完成,

福大大架构师每日一题

算法 rust 福大大

KCL - 让 Kubernetes 资源清单管理更容易

Peefy

编程 Serverless Kubernetes #开源 #DevOps

flutter系列之:如丝般顺滑的SliverAppBar

程序那些事

flutter 程序那些事

RocketMQ 在网易云音乐的实践

Apache RocketMQ

RocketMQ 消息

卡塔尔世界杯出现了半自动越位识别技术、动作轨迹捕捉等黑科技。

汀丶人工智能

12月日更 12月月更 世界杯黑科技

【IntelliJ IDEA】【SVN】SVN详细的介绍和Idea中如何使用SVN

No8g攻城狮

ide svn Git Submodule git fetch IDEA DeBug

喜报 | 秒云获评2022(第二届)“金信通”金融科技创新应用优秀案例

MIAOYUN

金融科技 解决方案 信创

演讲实录|OpenMLDB 与阿里云 MaxCompute 生态集成

第四范式开发者社区

人工智能 数据库 开源 时序数据库 特征

JDK自带命令优化

@下一站

代码优化 12月日更 12月月更 jvm优化 java程序优化

Flink核心组件

穿过生命散发芬芳

flink 12月月更

架构实战营 2-5 微信红包分析随堂测验

西山薄凉

「架构实战营」

学习java开发技术应该如何入手

小谷哥

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