单元测试是一个老生常谈的事情,可是能够深入开发人心,又能够喜欢写单元测试的同学少之又少。单元测试似乎功不在当下的事情,业务代码快速完成需求,才是王道,在工作量评估的时候,如果开发同学说要花上若干天时间来写单测,需要延后几天发布,那么PD可能就会:!#@%5*))~@#,单元测试是一件有情怀,有技术素养,有远期收益的工作,
单元测试,是保证软件质量和效率的重要手段之一。能点进来看本文的,都是有质量追求的同学哈,这里不对单元测试的必要性作赘述,简单提一下单元测试的五点好处:
监测软件质量
提升项目效率
促进代码优化
增加重构自信
软件行为文档化
本文书写的目的,是期望对当前单元测试相关技术做一个梳理和总结,在写法上给一些示例,帮助同学对单元测试相关的工具有大体了解,并能根据示例写出合理的单元测试。
单元测试书写的基本顺序大致包括:定义测试类、标记测试方法、创建被测试实例、执行被测试方法、结果验证。这个过程中有三个难点:
Runner。
这里不把Runner理解为JUnit的运行器,这里理解为单元测试的基础框架。框架的目的是为了帮助我们做资源加载等事情,定制单元测试的模板,让我们能够专注于单测case本身的书写。因此选择一个优秀的测试框架是必要的。
我们常用的框架如:JUnit4,TestNG,IntlTest(IntlTestBlockJUnit4ClassRunner),springTest & springbootTest & SpringContainer4Test(SpringJUnit4ClassRunner)等。
Mocks。
Mocks无疑是为了让我们更稳定的运行单元测试,它能隔离环境和外部数据对单元测试的影响,使得结果可预测。更重要的一点:mocks屏蔽了其他代码块对被测试块的影响,使得我们做的是真正的”单元“代码的测试。
所以就这个点而言,我个人是主张分层&纯内部逻辑测试的,举两个例子说明:
1. web/serviec client层;service层;manager/biz层;DAO/外部接口层,层与层之间尽量不要真正地依赖运行(甚至是utils工具类,除非你自己判断它是稳定的,完全可结果预测的)。一方面是为了运行稳定;另一方面是各个层保证自己逻辑正确即可,这也是单元测试的目标:仅做单元代码块的逻辑检测;还有个明显的好处:能轻易获得想要的数据,满足不同分支的测试。
2. HSF接口不建议依赖真正的服务,也要通过mock方式处理,除了上面的原因,个人认为单元测试不是集成测试,目标和做法是不同的。
常用的Mock工具:JMockit,此外还有EasyMock、jMock、Mockito、PowerMock等。
Assert
这里把assert也拿出来说一说,是因为我个人之前一直使用junit自带的Assert工具做断言,对于Hamcrest有一些了解,它自身封装了很多匹配器,也更加贴近自然语言,所以结合Junit的Assert能够在一定程度上支持复杂逻辑的断言。但是用过AssertJ以后,就决定只用它了。另外对专门做json断言的JSONassert也会做一些介绍。
// TODO By 4.15
优点:它是基础的assert工具,能满足全部断言需求。提供的api为assertTrue/assertEquals/assertNull/assertSame/assertArrayEquals等。其中assertThat(T actual, Matcher super T> matcher) 方法提供了一个可扩展接口。
缺点:JUnit的assert不对各类数据类型做逻辑封装(如String#substring()类似方法要自己调用解决),因此复杂数据类型或者复杂逻辑的校验,一方面需要我们自己实现断言的内容,另一方面要拆成多个独立的断言处理。因此使用起来比较麻烦,代码看上去也比较臃肿,语义不够直观。
定义。首先说hamcrest自身并不是一个单元测试框架,它本质上是一个包含很多有用的匹配器的库。可以使用在很多场景,尤其适合用于对:org.junit.Assert.assertThat(T actual, Matcher super T> matcher)做匹配功能的扩展,所以可以配合一起使用。
优点。
语义更加贴近自然语言,易于理解;
起源于java,另外多种语言提供支持,如Java, C++, Objective-C, Python, PHP, Ruby, Swift等。
缺点
不支持流式检查,对于一个结果做多维度的判断仍需要拆分断言;
api不够丰富,很多领域对象没有对应方法,如Date,Exception等;
发展较慢,对新技术的支持不到位。
api。下图为常用匹配器总览。
由于assert工具在这里推荐AssertJ,所以例子放多一些。
这是个其他断言没有的一个特色功能,所以说明一下。普通的单测方法在第一个检查失败时就结束跳出。SoftAssert提供了一个全部执行的功能,即全部断言都会运行,并打印失败结果。
优点:从上面一些demo中不难看出AssertJ的以下优点。
流式断言,对一个对象可以根据需要在一行代码中使用api接连断言,代码量少且优雅;
api可读性更好,更加贴近自然语义,AssertJ中封装了海量的api,基本都可以从名字中明确理解含义;
api库更强大。除了以上基础类型和异常、日期、类属性、soft断言api,更突出的优势是扩展了对以下领域的支持:DB(据说适配myBatis, Hibernate, JOOQ等多种DB框架)、Guava、Swing;Uri、xml、file;jdb8如:Future,Stream, Optional, Java 8 Date等。
开源&免费,对新技术支持迅速。当前许多新技术都
缺点:待考察。
定义:JSONassert是个很轻量的工具包,封装处理了JSONObject、JSON String、JSONArray的比较逻辑。旨在:让开发者对json的单测写更少的代码,并且适合做REST interfaces的测试。
优点:JSONassert会将string转换为JSONObject,并且结合对象的逻辑结构和数据做比较。它提供了两个维度的比较选择:是否容忍数据顺序不一致(推荐),是否容忍数据扩展,即可以选择:被比较对象增加了部分属性(忽略比较),只比较相同的属性部分。(JSONassert converts your string into a JSON object and compares the logical structure and data with the actual JSON. When strict is set to false (recommended), it forgives reordering data and extending results (as long as all the expected elements are there), making tests less brittle.)
缺点:个人觉得JSONCompareMode理解性上不是太好哈,而且compare返回的是个Result Pojo,需要自己用"_success"属性判断是否成功,封装性待考虑哈。跟AssertJ的api的设计还是有差距的。不过目前看json的断言比较好的工具就是JSONassert,大家可以自己体验下。
从以上介绍的顺序也能看出assert工具的一个逐渐进化的过程:
api从计算机的表达方式逐渐转为自然语义;
从单个断言到匹配器组合,再到任意扩展的流式断言;
对领域模型的封装逐渐做到强大,以及对java新技术栈的支持。
总结一句:推荐使用强大的AssertJ作为你的断言工具。
这里以jmockit(1.31版本)为例作说明,目的是想让大家在使用mock之前了解一下mock的过程,以及Instrumentation基于JVM的动态代理技术 & ASM字节码技术。知其然亦知其所以然。
另外, 其他Mock工具的动态代理的思路是一致的,但是具体技术不同,例如EasyMock、jMock、Mockito等对于接口的mock是基于java.lang.reflect.Proxy技术,生成一个新的实现类并在运行时AOP替换;或者对于非接口的非final类使用CGLIB技术动态生成子类。不过这样的技术处理会导致final类、构造方法以及一些不能被覆盖的方法不能被mock,有兴趣的同学可以挑一个框架研究一下源码。
Instrumentation。
解决问题: Instrumentation实现了JVM运行时对类进行动态控制和解释的这样一个动态代理。
具体说明:Instrumentation基于JAVA 5之前的JVMTI(Java Virtual Machine Tool Interface:JVM本地编程工具接口集合)技术,能够对虚拟机进行类定义的改变和操作,除此之外,能够处理虚拟机内存管理,线程控制等。在Jmockit中就通过调用Instrumentation#redefineClasses(ClassDefinition... definitions)方法结合MockUp新类的字节码实现类转换;其次,结合JAVA 6的Attach VirtualMachine工具,并通过编写agentmain指定方法,能够进行实时的动态操作;此外还有prefix-instrumentation方式,支持多种定制方法名替换原有native方法,并且在使用时,如果本地类库中找不到目标prefix方法,还可以尝试做原标准方法的解析。
框架初始化。Jmockit实现了自己的junit Runner,在Test框架初始化的时候,会一并初始化自己的动态代理,即Instrumentation的运行环境和初始数据。
期望录制。主要是对声明为MockUp对象的类和方法转化为字节码数组,通过Instrumentation对内存方法进行重新定义,并注册到本地的环境变量。
替换预期值。执行测试方法,通过Instrumentation的动态代理监听MockUp类调用,并替换录制结果,实现虚拟机级别的AOP功能。
接口(Impl)、抽象类(Impl)、抽象类(Subclass_)、普通类三种mock方式
以接口mock举例:根据mockup类创建mock的实现类;并且将mock类和实例注册到MockClasses的环境变量中;用当前mock类的字节码数组通过instrumentation改变被mock的类和方法。
instrumentation的运行时jvm通讯接口
从网上的一个表格能够清楚的看到几类常用mock工具的能力差别:
基于上面的比较,能看出JMockit功能是强大的,因此做推荐。另外从源码里能看到JMockit对Junit4/5,spring-test,以及testNG等框架都分别实现装饰器,做了较好的适配;Instrumentation Attach的虚拟机支持:Bsd/HotSpot/Linut/Windows/Solaris等;此外Jmockit自带覆盖率(coverage)统计能力,也是其他mock框架不具有的。下面对Jmockit的api和常用的写法做一些示例说明。
JMockit有两种mock方式:
Behavior-oriented(Expectations & Verifications) :对mock目标代码的行为进行模仿,更像黑盒测试。
State-oriented(MockUp
): 基于状态的mock。可以对传入的参数进行检查、匹配,才返回某些结果,类似白盒;基本上可以mock任何代码或逻辑。@Mocked:被修饰的对象将会被Mock,对应的类和实例都会受影响
@Injectable:仅Mock被修饰的实例
@Capturing:可以mock接口以及其所有的实现类
@Mock:MockUp模式中,指定被Fake的方法
Expectations:期望,指定的方法必须被调用
StrictExpectations:严格的期望,指定方法必须按照顺序调用
Verifications:验证
VerificationsInOrder:有顺序的验证
Invocation:工具类,可以获取调用信息
Delegate:自己指定返回值,适合那种需要参数决定返回值的场景,只需指定匿名子类就可以。
MockUp:模拟函数实现
Deencapsulation:反射工具类
这里说明一下,不仅public方法, private、protected 或者包保护级别的方法,以及static、final、native本地方法都是可以像例子里一样mock处理的。
用途:有些诸如servicelocator等类在初始化时做一些上下文加载的事情,如果不想运行相关初始化逻辑,即可用$clinit()模拟掉。
每个被mock的方法包括构造方法,都可以选择性的在第一个参数的位置添加Invocation参数。作用很多,例如获取当前mock实例,或者当前mock方法被执行的次数;
Deencapsulation工具能使用反射技术,操作类外部不可见属性。这为那些专门设计的,字段在外部不可操控的类的测试提供了方便。
mock方法中可以通过调用mock实例的proceed()方法,来执行原来的真实的方法
SpringBoot测试框架说明:http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing
JUnit4 GitHub:https://github.com/junit-team/junit4/wiki
IntlTest:http://docs.alibaba-inc.com/pages/viewpage.action?pageId=42938516
AssertJ开源网站:http://joel-costigliola.github.io/assertj
AssertJ示例代码:https://github.com/joel-costigliola/assertj-examples/tree/master/assertions-examples
hamcrest官网:http://hamcrest.org/
JSONassert官网:http://jsonassert.skyscreamer.org
mockito+spring:https://www.atatech.org/articles/64357
JMockit官网教程:http://jmockit.org/tutorial.html
JMockit SourceCode & Samples:https://github.com/jmockit/jmockit1
EasyMock官网:http://easymock.org
EasyMock 使用方法与原理剖析:https://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/
Instrumentation简介:https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html