junit:junit_简而言之,JUnit:测试隔离

junit: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类型及其使用目的。 最后,简短介绍了测试双重框架及其优缺点,从而结束了本章。 希望它具有足够的平衡性,可以使您对该主题有一个全面的了解,而又不致于琐碎。 改进建议当然受到高度赞赏。

本教程的下一篇文章将介绍Runner和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章,可维护性,Tomek Kaczanowski,2013年

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

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

junit:junit

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

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

相关文章

python基本模块中的对象_Python 学习笔记 -- OS模块的常用对象方法

1 #这里列举在os模块中关于文件/目录常用的函数使用方法23 #这里需要注意下,在使用这些方法前记得导入os模块4 import os #导入os模块5 """6 os对象方法:7 os.getcwd() #返回当前工作目录8 os.chdir(path) #改变工作目录9 os.listdir(pat…

OCA第2部分中的Java难题

欢迎使用OCA的Java Puzzlers的第二部分。 在这一部分中,我们将看到一个有趣的案例,涉及Java 7附带的数字文字中的下划线分隔符。 在下面的类中,您可以在十进制文字中看到分隔符下划线。 还请注意,该类现在可以正常编译。 八进制是…

msflexgrid允许大选择_选择复式楼、跃层和别墅的装修业主如何做好家里的楼梯...

点击上面蓝色字体关注!装修图例 | 别墅装修 | 装潢装饰 | 样板楼梯 | 装修设计很多复式或者别墅的房子,楼梯是不可缺少的建筑,大部分楼梯是连接客厅以及卧室的,很多朋友都选择在楼梯上面铺地板,木地板的改装空间大&…

php程序xml有必要学习吗,对初学者非常有用的PHP技巧

对初学者非常有用的PHP技巧echo "I is : $i ;}echo print_footer();那么,为什么你应该做输出缓冲呢:你可以在将输出发送给浏览器之前更改它,如果你需要的话。例如做一些str_replaces,或者preg_replaces,又或者是在…

OCA第1部分中的Java难题

我在业余时间正在阅读Mala Gupta的Oracle认证Java SE程序员助理书,我对所学到的一些新知识感到惊讶。 有时候他们真的没有任何意义,有时候他们虽然有道理,但确实令人惊讶。 因此,在本系列文章中,我想将它们共享为“ Ja…

oracle创建数据库用户并授权,oracle创建数据库、表空间、用户并授权

1、创建数据库简单的方式是使用Database Configuration Assistant数据库配置工具根据向导创建2、创建表空间在实际使用中需要创建自定义的表空间和临时表空间2.1、创建表空间create tablespace tabspace_nameloggingdatafile E:\app\oratable_space\ tabspace_name_temp.dbfsiz…

mysql 查询 系统字段 自然日_Mysql查询用户留存/留存率问题用户n日(内)留存、某日新增用户n日(内)留存...

Mysql查询用户留存/留存率语法计算某日的客户在第n日再次出现的概率--用户n日留存率。计算某日的客户在某个时间段内再次出现的概率--用户n日内留存率。计算某日新增的用户在第n日再次出现的概率--新用户n日留存率。计算某日新增的用户在某个时间段内再次出现的概率--新用户n日…

ajax 示例_通过示例了解挥发

ajax 示例我们已经花了几个月的时间来稳定Plumbr中的锁定检测功能 。 在此期间,我们遇到了许多棘手的并发问题。 许多问题是独特的,但是一种特殊类型的问题一直反复出现。 您可能已经猜到了–滥用volatile关键字。 我们已经发现并解决了许多问题&#x…

oracle long转为string,实现全局拦截前端传入的Long类型id转String

1遇到的问题在开发过程中存在这样一种问题,我们使用的id主键主要有long类型和varchar类型当主键id为long类型并且长度超过16位,当返回给前端时,前端会出现js解析的参数如果是Long类型的并且长度过大就会出现精度丢失。这就会造成后台返回的值…

springboot 多线程_redis官方推荐:SpringBoot用这个,一键多线程

Lettuce是一个可伸缩的线程安全的Redis客户端,提供了同步,异步和响应式使用方式。 如果多线程避免阻塞和事务操作(如BLPOP和MULTI / EXEC),则多个线程可共享一个连接。 Lettuce使用通信使用netty。 支持先进的Redis功能,如Sentine…

oracle查询最高一条记录,oracle 查询已有记录,上一条记录,下一条记录

oracle可以使用 lead、lag 函数来查询已有记录的下一条、上一条记录。表结构如下:如要查询Staffno是6-1102的前一条记录select * from staff where staff_no(select c.p from (select staff_no,lag(staff_no,1,0) over (order by staff_no) as p from staff) c wh…

使用Maven进行增量构建

这是2020年,如果您要启动任何新的基于Java的项目,则应优先选择gradle,但由于某些原因,如果您仍然对Maven感兴趣,那么您可能会发现这篇文章有用。 Maven Java / scala编译器插件对增量编译提供了不错的支持&#xff0c…

夏末浅笑_2014年夏末大Java新闻

夏末浅笑正如即将到来的JavaOne那样 ,最近在Java社区中已经有很多重大新闻。 这篇文章简要地引用了其中的三个项目(Java SE 8更新,Java SE 9和Java EE 8),并对我发现是我在类路径/类加载器问题上见过的更清晰的文章之一…

php删除菜单栏,如何删除WordPress站点健康状态面板和菜单项

下面由WordPress教程栏目给大家介绍删除WordPress站点健康状态面板和菜单项的方法,希望对需要的朋友有所帮助!删除 WordPress 站点健康状态面板和菜单项WordPress站点健康功能始于 5.2 版,如不想显示这玩意,可以使用本文的方法删除…

colspan会影响内部单元格宽度失效_电感失效分析

电感失效分析01电感的作用我们通常所说的电感指的是电感器件,它是用绝缘导线(例如漆包线,沙包线等)绕制而成的电磁感应元件。在电路中,当电流流过导体时,会产生电磁场,电磁场的大小除以电流的大小就是电感。电感是衡量线圈产生电磁…

linux清空redis命令,使用Linux管道批量删除Redis的key

------------------------------------------------------Redis并没有提供批量删除记录的方法,这有时候很不方便,特别是重新初始化数据的时候。一般有两种做法:如果业务场景明确,可以通过DBID进行区分,Redis默认的DBID…

ZeptoN正在将程序放入Java

1.简介 Java编程语言或“ Java”于1995年引入。然而,在近25年的时间里,它增加了最初不是核心语言所具有的功能。 此类功能包括枚举,泛型,对基本C样式功能开关语句的许多增强,断言等。 Java是一种编程语言,随…

深入理解python面向对象_转:Python3 面向对象,较为深入的两个理解

一,1. 类的声明和创建对于 Python 函数来说,声明与定义类没什么区别,因为他们是同时进行的,定义(类体)紧跟在声明(含 class 关键字的头行[header line])和可选(但总是推荐使用)的文档字符串后面。同时,所有的方法也必须…

linux循环脚本while循环,Shell脚本while、until循环语句简明教程

一、while循环while循环用于不断执行一系列命令,也用于从输入文件中读取数据;命令通常为测试条件。其格式为:while 命令docommand1command2...commandNdone命令执行完毕,控制返回循环顶部,从头开始直至测试条件为假。以…

分披萨问题_比萨问题–建造者与装饰者

分披萨问题问题陈述 我们需要为一家披萨公司构建软件,该公司想要准备不同类型的披萨,例如鸡肉披萨,扁平面包,意大利辣香肠披萨和特制奶酪,并在上面放些配料。 让我们尝试看看哪种设计模式适合该问题说明以及在哪种情况…