简而言之,JUnit:测试隔离

作为顾问,我仍然经常遇到程序员,他们对JUnit及其正确用法的理解最多。 这使我有了编写多部分教程的想法,从我的角度解释了要点。

尽管存在一些有关使用该工具进行测试的好书和文章,但是也许可以通过本动手实践的方法来使一两个额外的开发人员对单元测试感兴趣,这将使他们值得付出努力。

注意,本章的重点是基本的单元测试技术,而不是JUnit功能或API。 在后面的帖子中将讨论更多后者。 用于描述这些技术的术语是基于Meszaros的xUnit测试模式 [MES]中提供的定义。

以前简而言之在JUnit上

本教程从“ Hello World”一章开始,介绍了测试的基本知识:如何编写,执行和评估它。 它继续进行后期测试结构 ,解释了通常用于构建单元测试的四个阶段(设置,练习,验证和拆卸)。

这些课程还附有一个一致的示例,以使抽象概念更易于理解。 它被证明了,一个测试用例是如何一点一点地增长的-从幸福的道路开始到极端的案例测试,包括预期的例外。

总的来说,要强调的是,测试不仅是简单的验证机,还可以作为一种低级规范。 因此,应该以人们可能想到的最高编码标准来开发它。

依存关系

一个巴掌拍不响
谚语

本教程中使用的示例都是关于编写一个简单的数字范围计数器,该计数器从给定值开始传递一定数量的连续整数。 指定单元行为的测试用例可能在摘录中看起来像这样:

public class NumberRangeCounterTest {private static final int LOWER_BOUND = 1000;private static final int RANGE = 1000;private static final int ZERO_RANGE = 0;private NumberRangeCounter counter= new NumberRangeCounter( LOWER_BOUND, RANGE );@Testpublic void subsequentNumber() {int first = counter.next();int second = counter.next();assertEquals( first + 1, second );}@Testpublic void lowerBound() {int actual = counter.next();assertEquals( LOWER_BOUND, actual );}@Test( expected = IllegalStateException.class )public void exeedsRange() {new NumberRangeCounter( LOWER_BOUND, ZERO_RANGE ).next();}[...]
}

注意,这里我使用了一个非常紧凑的测试用例,以节省空间,例如使用隐式夹具设置和异常验证。 有关测试结构化模式的详细讨论,请参见上一章 。

还要注意,我坚持使用JUnit内置功能进行验证。 我将在另一篇文章中介绍特定匹配器库( Hamcrest , AssertJ )的优缺点。

虽然NumberRangeCounter的初始描述足以使本教程开始,但细心的读者可能已经注意到,该方法显然有些幼稚。 例如,考虑程序的进程可能会终止。 为了能够在系统重新启动时正确地重新初始化计数器,它至少应保留其最新状态。

但是,保持计数器的状态涉及通过不属于单元(也就是被测系统(SUT))的软件组件(数据库驱动程序,文件系统API等)访问资源(数据库,文件系统等)。 这意味着单位取决于这些组件,Meszaros用术语“ 依赖组件”(DOC)描述

不幸的是,这在许多方面带来了与测试有关的麻烦:

  1. 根据我们无法控制的组件,可能会阻碍对测试规范的体面验证。 试想一下有时可能不可用的真实世界的Web服务。 尽管SUT本身可以正常工作,但这可能是导致测试失败的原因。
  2. DOC也可能会减慢测试的执行速度。 为了使单元测试能够充当安全网 ,正在开发的系统的完整测试套件必须经常执行。 仅当每个测试运行得很快时,这才可行。 再次考虑Web服务示例。
  3. 最后但并非最不重要的一点是,例如,由于使用了较新版本的第三方库,DOC的行为可能会意外更改。 这说明了如何直接依赖我们无法控制的组件使测试变得脆弱

那么,我们该如何解决这个问题呢?

隔离–单元测试员的SEP字段

所谓SEP是我们不能看,或者不看,还是我们的大脑并没有让我们看到的,因为我们认为这公司的S omebodyËLSEP&roblem ...。
福特长官

由于我们不希望单元测试依赖于DOC的行为,也不希望它们过慢或脆弱,因此我们努力使我们的单元尽可能不受软件所有其他部分的影响。 简单地说,我们使这些特殊问题成为其他测试类型的关注点-因此开玩笑的SEP Field报价。

通常,此原理称为SUT隔离,表示希望分别测试关注点并保持测试彼此独立 。 实际上,这意味着应该以一种可以将每个DOC替换为所谓的Test Double的方式来设计单元, Test DoubleTest [MES1]的轻量级替代组件。

与我们的示例相关,我们可能决定不直接从单元本身内部访问数据库,文件系统等。 相反,我们可以选择将此问题分为屏蔽接口类型,而不必关心具体实现的外观。

尽管从低级设计的角度来看,这种选择当然也是合理的,但它并不能说明在整个测试过程中如何创建,安装和使用双重测试。 但是在详细介绍如何使用双打之前,还有一个主题需要讨论。

间接输入和输出

输入输出

到目前为止,我们的测试工作仅以SUT的直接输入和输出面对我们。 即, NumberRangeCounter每个实例都配有一个下限和一个范围值(直接输入)。 并且在每次调用next() ,SUT返回一个值或引发一个异常(直接输出),用于验证SUT的预期行为。

但是现在情况变得更加复杂了。 考虑到DOC为SUT初始化提供了最新的计数器值, next()的结果取决于该值。 如果DOC以这种方式提供SUT输入,我们将讨论间接输入

相反,假设next()每次调用都应保持计数器的当前状态,则我们没有机会通过SUT的直接输出进行验证。 但是我们可以检查计数器的状态是否已委托给DOC。 这种委托称为间接输出

有了这些新知识,我们应该准备继续进行NumberRangeCounter示例。

使用存根控制间接输入

从我们学到的知识来看,将计数器的状态保存分为自己的类型可能是个好主意。 这种类型会将SUT与实际的存储实现隔离开来,因为从SUT的角度来看,我们对如何实际解决保留问题不感兴趣。 因此,我们引入了CounterStorage接口。

尽管到目前为止还没有真正的存储实现,但我们可以使用测试倍数来代替。 由于接口尚无方法,因此此时创建测试双重类型很简单。

public class CounterStorageDouble implements CounterStorage {
}

为了以松散耦合的方式为NumberRangeCounter提供存储,我们可以使用依赖注入 。 通过两次存储测试来增强隐式夹具设置,然后将其注入到SUT中,如下所示:

private CounterStorage storage;@Beforepublic void setUp() {storage = new CounterStorageDouble();counter = new NumberRangeCounter( storage, LOWER_BOUND, RANGE );}

修复编译错误并运行所有测试后,该栏应保持绿色,因为我们尚未更改任何行为。 但是现在我们希望对NumberRangeCounter#next()的第一次调用尊重存储的状态。 如果存储提供的值n在计数器的定义范围内,则next()的第一次调用也应返回n ,这由以下测试表示:

private static final int IN_RANGE_NUMBER = LOWER_BOUND + RANGE / 2;[...]@Testpublic void initialNumberFromStorage() {storage.setNumber( IN_RANGE_NUMBER );int actual = counter.next();assertEquals( IN_RANGE_NUMBER, actual );}

我们的测试IN_RANGE_NUMBER必须提供确定性的间接输入,在我们的情况下为IN_RANGE_NUMBER 。 因此,它使用setNumber(int)来配备值。 但是由于尚未使用存储,因此测试失败。 要更改此设置,是时候声明CounterStorage的第一个方法了:

public interface CounterStorage {int getNumber();
}

这使我们可以像这样实现双重测试:

public class CounterStorageDouble implements CounterStorage {private int number;public void setNumber( int number ) {this.number = number;}@Override  public int getNumber() {return number;}
}

如您所见,double通过返回由setNumber(int)馈送的配置值来实现getNumber() setNumber(int) 。 以这种方式提供间接输入的测试双称为存根 。 现在,我们将能够实现NumberRangeCounter的预期行为并通过测试。

如果您认为get / setNumber用不好的名字来描述存储的行为,我同意。 但这简化了职位的演变。 请感到受邀提出构思周到的重构建议…

间谍的间接输出验证

为了能够在系统重启后恢复NumberRangeCounter实例,我们希望计数器的每个状态更改都将保留。 这可以通过在每次调用next()时将当前状态分配到存储中来实现。 因此,我们向DOC类型添加了一个setNumber(int)方法:

public interface CounterStorage {int getNumber();void setNumber( int number );
}

新方法与用于配置存根的签名具有相同的签名,这真是一个奇怪的巧合! 在使用@Override修改该方法后,很容易将我们的夹具设置重新用于以下测试:

@Testpublic void storageOfStateChange() {counter.next();assertEquals( LOWER_BOUND + 1, storage.getNumber() );}

与初始状态相比,我们期望在调用next()之后,计数器的新状态将增加一个。 更重要的是,我们希望这个新状态作为间接输出传递到存储DOC。 不幸的是,我们没有看到实际的调用,因此我们在double的局部变量中记录了调用的结果。

如果记录的值与预期值相匹配,则验证阶段将推断出正确的间接输出已传递到DOC。 上面以最简单的方式描述的记录状态和/或行为以供以后验证,也称为间谍。 因此,使用这种技术的测试两倍被称为间谍

那Mo子呢?

还有一种可能通过使用模拟来验证next()的间接输出。 这种类型的double的最重要的特征是,间接输出验证是在委托方法内部执行的。 此外,它还可以确保实际调用了预期的方法:

public class CounterStorageMock implements CounterStorage {private int expectedNumber;private boolean done;public CounterStorageMock( int expectedNumber ) {this.expectedNumber = expectedNumber;}@Overridepublic void setNumber( int actualNumber ) {assertEquals( expectedNumber, actualNumber );done = true;}public void verify() {assertTrue( done );}@Overridepublic int getNumber() {return 0;}
}

CounterStorageMock实例通过构造函数参数配置了期望值。 如果setNumber(int) ,则立即检查给定值是否与预期值匹配。 一个标志存储该方法已被调用的信息。 这允许使用verify()方法检查实际的调用。

这就是使用模拟的storageOfStateChange测试的外观:

@Testpublic void storageOfStateChange() {CounterStorageMock storage= new CounterStorageMock( LOWER_BOUND + 1 );NumberRangeCounter counter= new NumberRangeCounter( storage, LOWER_BOUND, RANGE );counter.next();storage.verify();}

如您所见,测试中没有剩下任何规格验证。 通常的测试结构有些扭曲,这似乎很奇怪。 这是因为验证条件是在夹具设置中间的运动阶段之前指定的。 验证阶段仅保留模拟调用检查。

但是作为回报,模拟可以在行为验证失败的情况下提供精确的堆栈跟踪,这可以简化问题分析。 如果再次查看间谍解决方案,您将认识到失败跟踪只会指向测试的验证部分。 没有关于实际上导致测试失败的生产代码行的信息。

这与模拟完全不同。 跟踪将使我们能够准确识别setNumber(int)调用位置。 有了这些信息,我们可以轻松地设置断点并调试问题。

由于这篇文章的范围,我只限于对存根,间谍和模拟进行双重测试。 有关其他类型的简短说明,您可以查看Martin Fowler的文章TestDouble ,但是可以在Meszaros的xUnit测试模式书[MES]中找到所有类型及其变型的深入说明。

在Tomek Kaczanowski的书《 使用JUnit和Mockito [KAC]进行实际单元测试 》中可以找到基于测试双重框架的模拟与间谍的良好比较(请参阅下一节)。

阅读本节后,您可能会觉得编写所有这些测试双打是繁琐的工作。 毫不奇怪,已编写库来大大简化双重处理。

测试双重框架–应许之地?

如果您只有锤子,那么一切看起来就像钉子
谚语

开发了一些框架以简化使用测试双打的任务。 不幸的是,就精确的测试双重术语而言,这些库并不总是一件好事。 例如, JMock和EasyMock专注于模拟 ,而Mockito却以间谍为中心。 也许这就是为什么大多数人都在谈论嘲笑 ,而不管他们实际上在使用哪种类型的双人间。

但是,有迹象表明 ,Mockito当时是首选的双重测试工具。 我猜这是因为它提供了良好的阅读流利的接口API,并通过提供详细的验证失败消息来弥补上述间谍提及的缺点。

无需详细介绍,我提供了一个storageOfStateChange()测试版本,该版本使用Mockito进行间谍创建和测试验证。 请注意, mockverifyMockito类型的静态方法。 通常的做法是将静态导入与Mockito表达式一起使用以提高可读性:

@Testpublic void storageOfStateChange() {CounterStorage storage = mock( CounterStorage.class );NumberRangeCounter counter = new NumberRangeCounter( storage, LOWER_BOUND, RANGE );counter.next();verify( storage ).setNumber( LOWER_BOUND + 1 );}

关于是否使用此类工具的文章很多。 例如,罗伯特·C·马丁(Robert C. Martin) 更喜欢手写双打 ,迈克尔·博尔迪沙(Michael Boldischar)甚至认为嘲笑框架有害 。 在我看来,后者只是在简单地滥用 ,而我一次不同意马丁所说的“写那些嘲笑是微不足道的”。

在发现Mockito之前,我多年来一直在使用手写双打。 立刻,我被卖给了流利的存根语法 (一种直观的验证方式),我认为摆脱那些笨拙的双精度类型是一种改进。 但这当然是情人眼中的。

但是,我经历了双重测试工具的诱惑,诱使开发人员过度操作。 例如,用双倍替换第三方组件非常容易,否则创建起来可能会很昂贵。 但这被认为是不好的做法, 史蒂夫·弗里曼 ( Steve Freeman)和纳特·普赖斯 ( Nat Pryce )详细解释了为什么模拟自己拥有的类型 [FRE_PRY]。

第三方代码要求进行集成测试和抽象适配器层 。 后者实际上就是我们在示例中通过引入CounterStorage所指示的内容。 而且,由于我们拥有适配器,因此可以安全地将其替换为双适配器。

一个容易进入的第二个陷阱是编写测试,其中一个测试双精度返回另一个测试双精度。 如果到了这一点,您应该重新考虑正在使用的代码的设计。 这可能会破坏demeter的定律 ,这意味着对象耦合在一起的方式可能有问题。

最后但并非最不重要的一点是,如果您考虑使用双重测试框架,则应记住,这通常是影响整个团队的长期决策。 由于代码风格的一致性,混合使用不同的框架可能不是最好的主意,即使您仅使用一种框架,每个(新)成员也必须学习特定于工具的API。

在开始广泛使用双打测试之前,您可能会考虑阅读比较经典测试与模拟测试的马丁·福勒的“ 莫克斯不是存根” ,或罗伯特·C·马丁的“ 何时模拟” ,其中介绍了一些启发式方法,以找出双打和太多之间的黄金比例。加倍。 或如Tomek Kaczanowski所说:

“很高兴您可以嘲笑一切,是吗? 放慢速度,并确保您确实需要验证交互。 你可能没有。 [KAC1]

结论

简而言之,JUnit的这一章讨论了单元依赖性对测试的影响。 它说明了隔离的原理,并说明了如何通过用测试双倍替换DOC来将其付诸实践。 在这种情况下,提出了间接输入和输出的概念,并描述了其与测试的相关性。

该示例通过动手示例加深了知识,并介绍了几种测试double类型及其使用目的。 最后,简短介绍了测试双重框架及其优缺点,从而结束了本章。 希望它具有足够的平衡性,可以使您对该主题有一个全面的了解,而又不致于琐碎。 改进建议当然受到高度赞赏。

本教程的下一篇文章将介绍Runners和Rules等JUnit功能并通过正在进行的示例展示如何使用它们。

参考文献

[MES] xUnit测试模式,Gerard Meszaros,2007年


[MES1] xUnit测试模式,第5章,原理:隔离SUT,Gerard Meszaros,2007年


[KAC]使用JUnit和Mockito进行实用单元测试,附录C。TestSpy vs. Mock,Tomek Kaczanowski,2013年

[KAC1]错误测试,良好测试,第4章,可维护性,托梅克·卡扎诺夫斯基,2013年

[FRE_PRY]不断增长的面向对象软件,由测试指导,第8章,史蒂夫·弗里曼(Steve Freeman),纳特·普莱斯(Nat Pryce),2010年

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

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

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

相关文章

Apache Camel 2.14中的更多指标

Apache Camel 2.14将于本月晚些时候发布。 由于正在解决某些Apache基础结构问题,因此存在一些问题。 这篇博客文章讨论的是我们添加到此版本中的新功能之一。 感谢Lauri Kimmel捐赠了骆驼指标组件,我们将其与出色的Codehale指标库集成在一起。 因此&am…

移动端网页宽度值(未加meta viewport标签)

移动端网页宽度值(未加meta viewport标签): iphone:980px Galaxy(盖乐世):980px Nexus:980px blackberry(黑莓):980px LG:980px Nokia:980p…

简而言之:JRunner

关于JUnit测试要点的多篇教程的第四章介绍了该工具可交换测试运行器体系结构的目的,并介绍了一些可用的实现。 正在进行的示例通过编写参数化测试的不同可能性扩大了主题。 由于我已经发布了JUnit Rules的介绍,因此我决定跳过关于该主题的已宣布部分。 …

cmake how to create vs file filters

cmake how to create vs file filters 用cmakelists构建出来的工程,没有文件filters,可采用如下方法解决 set(SOURCE_LIST"lotteryTicket.cpp""stdafx.cpp""stdafx.h""test/main.cpp" )add_executable(lotteryT…

Hibernate核心接口

一、Configuration类:1、 作用:(1)管理hibernate配置信息(2)读取hibernate.cfg.xml文件(3)加载hibernate的驱动,例如:url,用户名(4)管…

CSS实现垂直居中的方法

CSS实现垂直居中的方法 1、relative absolute定位&#xff1a; (1)css html代码 1 <!doctype html>2 <html lang"en">3 4 <head>5 <meta charset"UTF-8" />6 <title>Document</title>7 …

高并发系统之大忌-慢查询

最近又遇到了一次慢查把db&#xff08;mariadb10)几乎打挂的案例&#xff0c;作为一个核心支付系统的技术负责人&#xff0c;真是每日如履薄冰。因为之前支付系统经常出问题&#xff0c;现在各个BG对支付系统都盯得很紧。这次要不是我及时让DB给暴力清理数据&#xff0c;没准又…

Hadoop namenode启动瓶颈分析

转载&#xff1a;http://blog.csdn.net/AE86_FC/archive/2010/08/26/5842020.aspx NameNode启动过程详细剖析 NameNode中几个关键的数据结构 FSImage Namenode会将HDFS的文件和目录元数据存储在一个叫fsimage的二进制文件中&#xff0c;每次保存fsimage之后到下次保存之间的所有…

Java 9 –终极功能列表

这篇文章将针对即将到来的Java 9版本进行更新&#xff0c;新增功能 &#xff08; 最新更新&#xff1a;2014年 9月9日 &#xff09; OpenJDK开发正在加快速度&#xff1a;2014年3月Java 8发布之后&#xff0c;我们预计将进入2年的发布周期。 据报道&#xff0c;Java 9将于2016…

js中的作用域和作用域链

作用域就是变量与函数的可访问范围。在js中只有 全局作用域 和 函数作用域 &#xff0c;并没有块级作用域。 全局作用域 在所有函数外定义的变量、声明的函数就是全局作用域&#xff0c;在全部环境下都可以访问。 var a 111;function fn(){console.log(a); }fn(); // 打印了…

vue打包后不使用服务器直接访问方法

根据官网打包执行npm run build 后dist文件夹打开的index.html 是空白 需要开启http服务器才能访问&#xff0c;以下是解决办法 1、找到config文件夹下的index文件 修改成 2、找到build文件夹下的until文件 修改成 然后执行npm run build重新打包下就ok了 更多专业前端知…

OpenStack虚机网卡的创建过程

原文&#xff1a;https://www.sdnlab.com/20286.htmlOpenStack最基本和常用的操作就是启动虚机。虚机启动的过程中涉及很多内容&#xff0c;其中非常重要的一个环节就是创建并绑定虚机的虚拟网卡。虚机的创建和管理是Nova的任务&#xff0c;虚机网络的创建和管理是Neutron的任务…

js中的原型与原型链

js的学习有三座大山&#xff0c; 原型/原型链 、 作用域/闭包 、 异步/单线程&#xff0c;这三个知识点虽然基础但是入门时理解起来比较困难&#xff0c;本文先整理总结原型和原型链这一知识点。 1. 原型链怎么来的&#xff1f;对象的原型和function的prototype属性有什么关系…

HTML5 audio 如何实现播放多个MP3音频

<audio>标签是HTML5中的新标签&#xff0c;定义声音用于嵌入音频内容&#xff0c;比如音乐或其他音频流。用的比较多音频格式是.mp3。 <audio>标签常用属性如下表 属性值描述autoplayautoplay添加该属性后&#xff0c;音频会自动播放controlscontrols设置后&…

windwos下ntp服务器配置 arm平台ntp客户端获取同步时间

项目需要使用同步时间&#xff0c;在arm-linux开发板上&#xff0c;移植了ntp客户端&#xff0c;查看了一些资料&#xff0c;最终发现使用windows自带的ntp服务器比较方便&#xff0c;而且很靠谱&#xff0c;使用配置了一番&#xff0c;已经能够正常使用 详细步骤&#xff1a; …

BOM(Browser Object Model)

BOM&#xff08;浏览器对象模型&#xff09;&#xff0c;提供了一系列操作浏览器&#xff0c;获取浏览器信息的接口。这些接口在平时的工作中会经常用到&#xff0c;例如当前页面的刷新&#xff0c;获取url的参数等等。 注&#xff1a;图片来自 http://www.dreamdu.com/javascr…

入门 IT 行业,该具备哪些技能?

对于刚开始进入IT的新人来说&#xff0c;“必备技能”往往意味着一个长长的、标有重要度的学习列表&#xff0c;但是过长的列表通常会导致新人不知如何开始学习&#xff0c;压力倍增。本文尝试列举出最重要的几个技能&#xff0c;也期望通过此列表能给新人一个比较明确的学习重…

实验七作业

Part 1:验证性实验 将line29&#xff1a;for(i0;i<N;i)改为while(!feof(fp)) // 从文本文件file1.dat中读取数据&#xff0c;找出最高分和最低分学生信息&#xff0c;并输出在屏幕上 #include <stdio.h> #include <stdlib.h>#define N 10// 定义一个结构体类型…

块级格式化上下文(Block Formatting Context)

CSS块级格式化上下文是块级盒子的一种能力&#xff0c;这种能力并不是直接通过css属性声明而获得的&#xff0c;而是添加css的一部分相关属性之后自动获得的能力&#xff0c;也就是说没有一个明确的属性就是生成块级格式化上下文的。 块级格式化上下文的能力就是让具有该能力的…

前端性能优化方法总结

一个网站前端性能的好坏很大程度上影响了用户愿不愿意使用访问这个网站&#xff0c;因此对前端进行性能优化是个很重要的事情。  对于前端性能优化这个问题&#xff0c;主要学习自yahoo前端性能团队总结的35条黄金定律总结&#xff0c;觉得很全很赞&#xff0c;做个学习总结和…