简而言之,JUnit:单元测试断言

简而言之,本章涵盖了各种单元测试断言技术。 它详细说明了内置机制, Hamcrest匹配器和AssertJ断言的优缺点 。 正在进行的示例扩大了该主题,并说明了如何创建和使用自定义匹配器/断言。

单元测试断言

信任但要验证
罗纳德·里根(Ronald Reagan)

岗位测试结构解释了为什么单元测试通常分阶段进行。 它澄清说, 真正的测试结果验证在第三阶段进行。 但是到目前为止,我们只看到了一些简单的示例,主要使用了JUnit的内置机制。

如Hello World所示,验证基于错误类型AssertionError 。 这是编写所谓的自检测试的基础。 单元测试断言将谓词评估为truefalse 。 如果为false ,则抛出AssertionError 。 JUnit运行时捕获此错误并将测试报告为失败。

以下各节将介绍三种较流行的单元测试断言变体。

断言

JUnit的内置断言机制由类org.junit.Assert 。 它提供了两种静态方法来简化测试验证。 以下代码片段概述了可用方法模式的用法:

fail();
fail( "Houston, We've Got a Problem." );assertNull( actual );
assertNull( "Identifier must not be null.",actual );assertTrue( counter.hasNext() );
assertTrue( "Counter should have a successor.",counter.hasNext() );assertEquals( LOWER_BOUND, actual );
assertEquals( "Number should be lower bound value.", LOWER_BOUND,actual );
  1. Assert#fail()无条件地引发断言错误。 这对于标记不完整的测试或确保引发了预期的异常可能很有帮助(另请参见“ 测试结构”中的“预期异常”部分)。
  2. Assert#assertXXX(Object)用于验证变量的初始化状态。 为此,存在两个称为assertNull(Object)assertNotNull(Object)
  3. Assert#assertXXX(boolean)方法测试boolean参数传递的预期条件。 调用assertTrue(boolean)期望条件为true ,而assertFalse(boolean)期望相反。
  4. Assert#assertXXX(Object,Object)Assert#assertXXX(value,value)方法用于对值,对象和数组进行比较验证。 尽管结果没有区别,但通常的做法是将期望值作为第一个参数,将实际值作为第二个参数。

所有这些类型的方法都提供带有String参数的重载版本。 如果发生故障,此参数将合并到断言错误消息中。 许多人认为这有助于更清楚地指定失败原因。 其他人则认为这样的消息混乱,使测试更难以阅读。

乍一看,这种单元测试断言似乎很直观。 这就是为什么我在前面的章节中使用它进行入门的原因。 此外,它仍然非常流行,并且工具很好地支持故障报告。 但是,在需要更复杂谓词的断言的表达性方面也受到一定限制。

Hamcrest

Hamcrest是一个旨在提供用于创建灵活的意图表达的API的库。 该实用程序提供了称为Matcher的可嵌套谓词。 这些允许以某种方式编写复杂的验证条件,许多开发人员认为比布尔运算符更易于阅读。

MatcherAssert类支持单元测试断言。 为此,它提供了静态的assertThat(T, Matcher )方法。 传递的第一个参数是要验证的值或对象。 第二个谓词用于评估第一个谓词。

assertThat( actual, equalTo( IN_RANGE_NUMBER ) );

如您所见,匹配器方法模仿自然语言的流程以提高可读性。 以下代码片段更加清楚了此意图。 这使用is(Matcher )方法来修饰实际的表达式。

assertThat( actual, is( equalTo( IN_RANGE_NUMBER ) ) );

MatcherAssert.assertThat(...)存在另外两个签名。 首先,有一个采用布尔参数而不是Matcher参数的变量。 它的行为与Assert.assertTrue(boolean)

第二个变体将一个附加的String传递给该方法。 这可以用来提高故障消息的表达能力:

assertThat( "Actual number must not be equals to lower bound value.", actual, is( not( equalTo( LOWER_BOUND ) ) ) );

在失败的情况下,给定验证的错误消息看起来像这样:

hamcrest-failure

Hamcrest带有一组有用的匹配器。 图书馆在线文档的“常见匹配项”部分列出了最重要的部分。 但是对于特定域的问题,如果有合适的匹配器,通常可以提高单元测试断言的可读性。

因此,该库允许编写自定义匹配器。

让我们返回教程的示例来讨论该主题。 首先,我们对该场景进行调整以使其更合理。 假设NumberRangeCounter.next()返回的是RangeNumber类型,而不是简单的int值:

public class RangeNumber {private final String rangeIdentifier;private final int value;RangeNumber( String rangeIdentifier, int value  ) {this.rangeIdentifier = rangeIdentifier;this.value = value;}public String getRangeIdentifier() {return rangeIdentifier;}public int getValue() {return value;}
}

我们可以使用自定义匹配器来检查NumberRangeCounter#next()的返回值是否在计数器定义的数字范围内:

RangeNumber actual = counter.next();assertThat( actual, is( inRangeOf( LOWER_BOUND, RANGE ) ) );

适当的自定义匹配器可以扩展抽象类TypeSafeMatcher<T> 。 该基类处理null检查和类型安全。 可能的实现如下所示。 请注意,它如何添加工厂方法inRangeOf(int,int)以便于使用:

public class InRangeMatcher extends TypeSafeMatcher<RangeNumber> {private final int lowerBound;private final int upperBound;InRangeMatcher( int lowerBound, int range ) {this.lowerBound = lowerBound;this.upperBound = lowerBound + range;}@Overridepublic void describeTo( Description description ) {String text = format( "between <%s> and <%s>.", lowerBound, upperBound );description.appendText( text );}@Overrideprotected void describeMismatchSafely(RangeNumber item, Description description ){description.appendText( "was " ).appendValue( item.getValue() );}@Overrideprotected boolean matchesSafely( RangeNumber toMatch ) {return    lowerBound <= toMatch.getValue() && upperBound > toMatch.getValue();}public static Matcher<RangeNumber> inRangeOf( int lowerBound, int range ) {return new InRangeMatcher( lowerBound, range );}
}

对于给定的示例,工作量可能会有些夸大。 但它显示了如何使用自定义匹配器消除先前帖子中有些神奇的IN_RANGE_NUMBER常量。 除了新类型外,还强制声明语句的编译时类型安全。 这意味着例如String参数将不被接受进行验证。

下图显示了使用自定义匹配器的测试结果失败的样子:

hamcrest-custom-failure

很容易看出describeTodescribeMismatchSafely的实现以哪种方式影响故障消息。 它表示期望值应该在指定的下限和(计算的)上限1之间 ,并跟在实际值之后。

不幸的是,JUnit扩展了其Assert类的API以提供一组assertThat(…)方法。 这些方法实际上复制了MatcherAssert提供的API。 实际上,这些方法的实现委托给这种类型的相应方法。

尽管这似乎是一个小问题,但我认为值得一提。 由于这种方法,JUnit牢固地与Hamcrest库绑定在一起。 这种依赖性有时会导致问题。 特别是与其他库一起使用时,通过合并自己的hamcrest版本的副本,情况更糟……

Hamcrest的单元测试主张并非没有竞争。 尽管关于每次测试一个确定每个测试 一个概念的讨论超出了本文的讨论范围,但后一种观点的支持者可能认为该库的验证声明过于嘈杂。 尤其是当一个概念需要多个断言时。

这就是为什么我必须在本章中添加另一部分!

断言

在“ 测试跑步者”中,示例片段之一使用了两个assertXXX语句。 这些验证期望的异常是IllegalArgumentException的实例并提供特定的错误消息。 该段看起来像这样:

Throwable actual = ...assertTrue( actual instanceof IllegalArgumentException );
assertEquals( EXPECTED_ERROR_MESSAGE, actual.getMessage() );

上一节教我们如何使用Hamcrest改进代码。 但是,如果您碰巧是该库的新手,您可能会想知道要使用哪个表达式。 或打字可能会感到不舒服。 无论如何,多个assertThat语句会加在一起。

AssertJ库努力通过为Java提供流畅的断言来改善这一点。 流畅的接口 API的目的是提供一种易于阅读的,富有表现力的编程风格,从而减少胶合代码并简化键入。

那么如何使用这种方法来重构上面的代码?

import static org.assertj.core.api.Assertions.assertThat;

与其他方法类似,AssertJ提供了一个实用程序类,该类提供了一组静态的assertThat方法。 但是这些方法针对给定的参数类型返回特定的断言实现。 这就是所谓的语句链接的起点。

Throwable actual = ...assertThat( actual ).isInstanceOf( IllegalArgumentException.class ).hasMessage( EXPECTED_ERROR_MESSAGE );

旁观者认为可读性在一定程度上可以扩展,但无论如何都可以用更紧凑的样式来写断言。 了解如何流畅地添加与被测特定概念相关的各种验证方面。 这种编程方法支持有效的类型输入,因为IDE的内容辅助可以提供给定值类型的可用谓词列表。

因此,您想向后世提供表现力的失败消息吗? 一种可能性是使用describedAs作为链中的第一个链接来注释整个块:

Throwable actual = ...assertThat( actual ).describedAs( "Expected exception does not match specification." ).hasMessage( EXPECTED_ERROR_MESSAGE ).isInstanceOf( NullPointerException.class );

该代码段期望使用NPE,但假设在运行时抛出了IAE。 然后失败的测试运行将提供如下消息:

断言失败

也许您希望根据给定的失败原因使您的消息更加细微。 在这种情况下,您可以每个验证规范之前添加一条describedAs语句:

Throwable actual = ...assertThat( actual ).describedAs( "Message does not match specification." ).hasMessage( EXPECTED_ERROR_MESSAGE ).describedAs( "Exception type does not match specification." ).isInstanceOf( NullPointerException.class );

还有更多的AssertJ功能可供探索。 但是,要使这篇文章保持在范围之内,请参阅实用程序的在线文档以获取更多信息。 但是,在结束之前,让我们再次看一下范围内验证示例。 这是可以通过自定义断言解决的方法:

public class RangeCounterAssertionextends AbstractAssert<RangeCounterAssertion, RangeCounter>
{private static final String ERR_IN_RANGE_OF = "Expected value to be between <%s> and <%s>, but was <%s>";private static final String ERR_RANGE_ID = "Expected range identifier to be <%s>, but was <%s>";public static RangeCounterAssertion assertThat( RangeCounter actual ) {return new RangeCounterAssertion( actual );}public InRangeAssertion hasRangeIdentifier( String expected ) {isNotNull();if( !actual.getRangeIdentifier().equals( expected ) ) {failWithMessage( ERR_RANGE_ID, expected, actual.getRangeIdentifier()  );}return this;}public RangeCounterAssertion isInRangeOf( int lowerBound, int range ) {isNotNull();int upperBound = lowerBound + range;if( !isInInterval( lowerBound, upperBound ) ) {int actualValue = actual.getValue();failWithMessage( ERR_IN_RANGE_OF, lowerBound, upperBound, actualValue );}return this;}private boolean isInInterval( int lowerBound, int upperBound ) {return actual.getValue() >= lowerBound && actual.getValue() < upperBound;}private RangeCounterAssertion( Integer actual ) {super( actual, RangeCounterAssertion.class );}
}

自定义断言是扩展AbstractAssert常见做法。 第一个通用参数是断言的类型本身。 流利的链接样式需要它。 第二种是断言所基于的类型。

该实现提供了两种附加的验证方法,可以按照以下示例进行链接。 因此,这些方法将返回断言实例本身。 请注意, isNotNull()的调用如何确保我们要在其上进行断言的实际RangeNumber不为null

定制断言由其工厂方法assertThat(RangeNumber) 。 因为它继承了可用的基本检查,所以断言可以开箱即用地验证非常复杂的规范。

RangeNumber first = ...
RangeNumber second = ...assertThat( first ).isInRangeOf( LOWER_BOUND, RANGE ).hasRangeIdentifier( EXPECTED_RANGE_ID ).isNotSameAs( second );

为了完整RangNumberAssertion ,以下是RangNumberAssertion的实际运行方式:

断言自定义失败

不幸的是,不可能在同一测试用例中将两种不同的断言类型与静态导入一起使用。 当然,假定这些类型遵循assertThat(...)命名约定。 为了避免这种情况,文档建议扩展实用程序类Assertions

这样的扩展可用于提供静态的assertThat方法,作为所有项目自定义断言的入口。 通过在整个项目中使用此自定义实用程序类,不会发生导入冲突。 在为所有断言提供单一入口点的部分中,可以找到详细的描述在线文档中有关定制断言的 yours + AssertJ

流利的API的另一个问题是单行链接的语句可能更难调试。 这是因为调试器可能无法在链中设置断点。 此外,可能不清楚哪个方法调用已引起异常。

但是,正如Wikipedia所说的那样,可以通过将语句分成多行来克服这些问题,如上面的示例所示。 这样,用户可以在链中设置断点,并轻松地逐行逐步执行代码。

结论

简而言之,JUnit的这一章介绍了不同的单元测试断言方法,例如该工具的内置机制,Hamcrest匹配器和AssertJ断言。 它概述了一些优缺点,并通过本教程的进行中示例对主题进行了扩展。 此外,还展示了如何创建和使用自定义匹配器和断言。

尽管基于Assert的机制肯定是过时的并且不太面向对象,但它仍然具有它的提倡者。 Hamcrest匹配器将断言和谓词定义完全分开,而AssertJ断言以紧凑且易于使用的编程风格进行评分。 所以现在您选择太多了……

请注意,这将是本教程有关JUnit测试要点的最后一章。 这并不意味着没有更多要说的了。 恰恰相反! 但这将超出此迷你系列量身定制的范围。 您知道他们在说什么: 总是让他们想要更多…

  1. 嗯,我想知道区间边界是否会比下限和范围更直观...

翻译自: https://www.javacodegeeks.com/2014/09/junit-in-a-nutshell-unit-test-assertion.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/362380.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue 安装使用mockjs

使用 mockjs 的案例过程&#xff1a; 1. 安装 npm install axios -S npm install mockjs --save-dev npm install --save 、--save-dev 、-D、-S 的区别与NODE_ENV的配置_jwl_willon的博客-CSDN博客_npm save备注&#xff1a;<> 意为等价于&#xff1b;1、npm install …

大数据学习——SparkStreaming整合Kafka完成网站点击流实时统计

1.安装并配置zk 2.安装并配置Kafka 3.启动zk 4.启动Kafka 5.创建topic [rootmini3 kafka]# bin/kafka-console-producer.sh --broker-list mini1:9092 --topic cyf-test 程序代码 package org.apache.sparkimport java.net.InetSocketAddressimport org.apache.spark.HashParti…

日期/时间格式/解析,Java 8样式

自Java 几乎 开始以来&#xff0c;Java开发人员就通过java.util.Date类&#xff08;自JDK 1.0起&#xff09;和java.util.Calendar类&#xff08;自JDK 1.1起 &#xff09;来处理日期和时间。 在这段时间内&#xff0c;成千上万&#xff08;甚至可能数百万&#xff09;的Java开…

无废话WPF系列5:控件派生图

1. WPF类控件的派生关系图&#xff0c;紫色的部分开始才算是进入WPF的框架里。 2. WPF控件图 WPF的UI控件主要有以下类型&#xff0c;ContentControl, HeaderedContentControl, ItemsControl, HeaderedItemsControl, Panel, Adorner(文字点缀元素), Flow Text(流式文本元素), T…

安装ipython和jupyter

本节内容&#xff1b; 安装ipython安装jupyterPycharm介绍 Python软件包管理一、安装ipython 1. python的交互式环境2. 安装ipython 可以使用pip命令安装。如果你是用pyenv安装的python的话&#xff0c;pip命令已经有了。 当需要安装包的时候&#xff0c;最好进入虚拟环境&…

vue 图片资源应该如何存放并引入(public、assets)?

全局cli配置&#xff1a;vue.config.js 之前写项目就想着怎么简单怎么来&#xff0c;图片要用了&#xff0c;就直接在要用图片的页面&#xff0c;新建一个跟页面同等级的 imgs 文件夹&#xff0c;然后在页面中直接 “./imgs/图片.png”&#xff0c;不得不说这样写很方便。 但是…

layui 树形组件下拉框

采用 layui 树形组件&#xff0c;版本&#xff1a;V2.6.8。只需要更新layui版本&#xff0c;不需要下载tableSelect。 原作者博客&#xff1a;https://blog.csdn.net/m0_67402588/article/details/123526860。 从 官网 更新日志可以看到&#xff0c;树形组件在2.5.7版本还在更新…

layui table表格的复选框checkbox设置部分为不可选

需求&#xff1a;如上图&#xff0c;某些数据禁用删除功能&#xff0c;那么全选时&#xff0c;这些数据前面的复选框也不能选。 实现&#xff1a;在layui数据表格中设置了字段为 type:checkbox 但是想要实现部分不显示&#xff0c;不可选的功能。layui内置没有该功能&#xff…

layui upload阻止文件上传问题,及多选文件上传

1、效果展示&#xff1a; 2、需求&#xff1a; 下拉框及月份都为不空&#xff0c;且有文件数据才能提交上传。 3、环境&#xff1a; 目前项目中引用的 layui 版本是 2.4.5。在 before 中进行判断&#xff0c;使用 return false 想要阻止文件上传没反应&#xff0c;文件仍然会…

2019.06.17课件:[洛谷P1310]表达式的值 题解

P1310 表达式的值 题目描述 给你一个带括号的布尔表达式&#xff0c;其中表示或操作|&#xff0c;*表示与操作&&#xff0c;先算*再算。但是待操作的数字&#xff08;布尔值&#xff09;不输入。 求能使最终整个式子的值为0的方案数。 题外话 不久之前我在codewars上做过一…

vue+element 封装日期范围组件(双向绑定)

像这样的日期组件&#xff0c;在后台管理项目中是比较多的&#xff0c;而且加了快捷选项&#xff0c;代码量较多&#xff0c;因此封装成组件。 封装这一类型的组组件&#xff0c;主要是了解输入框双向绑定 v-model 的过程。 1、了解输入框双向绑定的过程&#xff1a; 官网&am…

用Hystrix保护您的应用程序

在之前的帖子http://www.javacodegeeks.com/2014/07/rxjava-java8-java-ee-7-arquillian-bliss.html中&#xff0c;我们讨论了微服务以及如何使用&#xff08;RxJava&#xff09;的Reactive Extensions编排微服务。 但是&#xff0c;如果一项或多项服务由于已被暂停或引发异常而…

若依 从下载到成功运行及打包

官网&#xff1a;http://www.ruoyi.vip/ 目录 一、下载并运行项目 二、关于 若依 接口地址配置 2.1 若依的跨域代理介绍 2.2 配置跨域代理&#xff0c;调用后台接口 2.2.1 配置 后台 ip 地址 2.2.2 页面报“系统接口404”错误 三、打包配置 3.1 打包之后静态资源404…

uniAPP小程序 子组件使用watch不生效,H5正常,小程序不正常(其实是子组件model选项的问题)

第一次用 uniapp 写小程序&#xff0c;还是遇到挺多问题的。写了一个下拉多选组件&#xff0c;发现同样的代码&#xff0c;在H5上运行效果正常&#xff0c;在小程序上压根不走 watch 。 uniapp官网&#xff1a;【全局配置 | uni-app官网】 看文档 watch 是支持H5、小程序的&…

jQuery EasyUI/TopJUI创建日期时间输入框

jQuery EasyUI/TopJUI创建日期时间输入框 日期时间输入框组件 HTML 和日期输入框类似&#xff0c;日期时间输入框允许用户选择日期和指定的时间并按照指定的输出格式显示。相比日期输入框&#xff0c;它在下拉面板中添加了一个时间微调器。 <div class"topjui-containe…

table 设置边框

本文引自&#xff1a;https://www.cnblogs.com/leona-d/p/6125896.html 示例代码&#xff1a; <!DOCTYPE html><html lang"zh"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width…

uniapp小程序 设置自定义导航栏

如下截图&#xff0c;通过 wx.getSystemInfoSync 计算得到的整个导航栏高度&#xff0c;其实是有3个部分的&#xff1a; 黄色&#xff1a;状态栏高度&#xff0c;uniapp文档中有给出&#xff1b;红色&#xff1a;胶囊高度&#xff0c;可以计算得出&#xff1b;绿色&#xff1a;…

Akka Notes –演员记录和测试

在前两部分&#xff08; 一 &#xff0c; 二 &#xff09;中&#xff0c;我们简要讨论了Actor以及消息传递的工作方式。 在这一部分中&#xff0c;让我们看一下如何修复并记录我们的TeacherActor 。 概括 这就是我们上一部分中的Actor的样子&#xff1a; class TeacherActor …

vue笔记(二)Vue-class与style、事件、计算属性、数据监听、指令+自定义指令、过滤器

vue官网 一 、class、style操作 二、事件 三、计算属性 四、数据监听、观测 五、指令自定义指令 六、过滤器 一 、class、style操作 官网 1. class使用&#xff1a; &#xff08;1&#xff09;v-bind:class“数据|属性|变量|表达式” &#xff08;2&#xff09;v-bind:class“…

Nsum问题

题目 题解 暴力法 class Solution:def fourSum(self, nums: List[int], target: int) -> List[List[int]]:if len(nums) < 4:return []nums.sort()N len(nums)res []for i in range(N-3):for j in range(i1, N-2):for k in range(j1, N-1):for m in range(k1, N):tmp…