为什么要单元测试:
-
帮助理解需求:开发人员在编写测试代码的时候,可以更加清楚的了解代码的结构和业务逻辑。
-
尽早的发现bug:在<快速软件开发>这本书中指出,根据大量的研究数据证明:最后才修改一个bug的代价是在bug产生时修改它的代价大10倍。
-
提高局部代码的质量:保证局部代码质量,我们才能保证各个依赖你的其他模块的代码质量。
-
成本:这里说的测试成本是相对而言的,比如:对于集成测试的复杂环境部署,单元测试显得相对简单点。笔者简单了解了下笔者公司的开发写个单元测试平均在0.5H左右。
-
单元测试可以被复用:一劳永逸。一些固化的功能模块,只要我们写好单元测试后,以后基本不需要调整,为实现单元测试自动化打好了基础。
当前单元测试遇到的问题:
-
本地测试代价大:笔者所在公司有将近10个系统,有时候开发一个简单的功能,如果不发布到集成环境测试,在本地做单元测试至少需要启动2-3个系统才可以跑起来,经常IDE卡死。而发布到集成环境测试则耗时较长且调式麻烦。
-
复杂性:开发需要关心各种环境配置项的值,才可以正确的启动系统做单元测试,如:证书、密钥、验签等安全配置。特别是像笔者所在的这种金融领域,各种安全配置、银行调用URL配置。
-
不可控性:跨公司、跨部门、跨系统的接口调用,导致单元测试的效率和结果不可控。
-
异常分支测试:笔者做的金融系统,有很多和银行交互的接口,有的时候需要连接一下银行的测试环境测试下“难于上青天啊”(小银行会简单的配合你下,大银行根本不鸟你,给你一个地址你自己玩去吧),更别说给你返回个异常数据了,连正常的数据都返回不了。另外比如:并发、系统压力测试(有的时候会选择硬编码将调用外部系统的地方写死,压力测试后再改回来上线,但是容易漏改、耗费人力、不能复用),还有当前很流行的各种分布式、大数据的单元测试都比较困难。
常见的解决方案
下面笔者简单分析下上述问题常见的解决方案(可能有更好的方案,这里笔者还没有想到,可以一起交流学习):
-
问题1-solve. 部署一套稳定的环境:专门给开发人员单元测试连接调用,但是也存在一些问题,比如我们经常发现使用dubbo这样的RPC调用框架,会出现消费者和服务者混乱,因为共用一套环境大家都把自己的服务注册上去了。数据库数据会被别的开发修改,调式半天发现数据被别的开发修改了,这种情况痛苦的一比。
最理想的是一人一套环境,只有财大气粗的公司如此了。
-
问题2-solve. 统一开发目录和配置项:对于安全配置的证书这些问题比较好解决,所有开发共用一套单元测试环境变量配置,证书路径和url都统一。不同操作系统不同模板
-
问题3-solve. 万年难:问题三这个就比较难搞定了,例如你正在测试自己的case,突然你调用的B部门的服务出问题了,你会经常听到类似:我擦,服务被关闭了?他们在发布新版本?返回的数据不对啊、怎么用户不存在?我靠,他们又刷库了?。一等就是千万年,无法忍受。
当然笔者也曾经试着将这些接口调用全部写死了。直接new一个结果返回,然而细 心的人提交代码的时候可能会检查下,不细心的则深挖坑啊。。。。。。
-
问题4-solve. 写死返回结果:在需要异常场景的时候,注释掉调用代码,写死返回结果。基本和3类似。
后来基于3和4的解决思路,慢慢的就演变出了一种专门解决这些场景的框架:Mock框架。这也符合笔者一直崇尚的理念:业务驱动开发,有需求就会诞生解决方案。
Mock框架初接触
笔者这里简单的说下自己的mock实现,如果有不足的地方还希望各路大神多多指教,相互学习成长。
-
选择TestNG : 首先做单元测试当然少不了junit或者testng了(也有NB的公司有自己的测试框架,这些公司呢想必也都是业务驱动逼迫自己去搞的),笔者这里就不阐述着两个测试框架的区别了,网上各种帖子,总之适合自己、用的熟练、懂得原理就可以。那笔者这里选择的是testng。
-
选择Jmockit作为mock框架:另外一个就是mock框架的选型。当前的江湖中,mock框架已经有很多门派了,但是万变不离其宗,他们要么是JDK的动态代理、要不就是CGLIB的动态代理生成新的类。比如:easymock 、mockito、jmock等已经风生水起了,但是笔者认为这样的实现原理决定了它的局限性,比如:final方法、构造方法、不能被覆写的方法这些就不能被mock了。
思路到这里暂停一些,我们来回忆下最初的我们:
记得很多年前,我们在刚学java的时候,大神们就教导我们学习java,首先我们得知道一个java文件是怎么最终被机器执行的。
我们写了一个Hello.java 然后通过cmd命令javac Hello.java经过javac的编译器后完成了对代码的词法分析、语法分析、抽象语法树,然后得到一个Hello.class,这部分是在JVM外面的完成的。
然后由JVM类加载到内存中(这里笔者就不叙述加载的过程了,网上可以搜到很多相关文章,再说笔者自己了解的也是皮毛)。到JVM以后,然后JVM翻译成机器码执行。
有了这样的背景知识,再让我们思考如何mock一个类,我们自然的会想到的去修改这个类中在JVM中的字节码,这样就没有什么不可以mock的了。我们知道从JDK1.5开始就提供了java.lang.instrument包,其中提供了修改JVM中已加载类的重定义入口即java.lang.instrument.Instrumentation#redefineClasses(ClassDefinition...)方法。
那当前江湖中有没有这样的一个框架可以和我们的思路很符合呢?笔者google到了这样的一种框架,那就是JMockit(笔者一直认为框架只要适合自己能够满足业务场景就好,无需过多的去追求时髦)。
Jmockit简单介绍:
JMockit:是googlecode上的一个项目衍生而来,现在已经有了自己的独立门户网站,http://jmockit.org/,官网介绍其是基于asm库来修改java的字节码从而达到篡改类的行为的mock工具。通过JDK提供的类重定义方法:java.lang.instrument.Instrumentation#redefineClasses(ClassDefinition...)作为修改JVM中类的定义的入口。
这样mock框架就定了。是时候我们设计下我们蓝图了。
融合TestNG和JMockit
-
框架设计:
-
AbstractMockBase:mock类的基类,为以后扩展预留。
-
TestAMock: 具体的Mock实现类。
-
mockContext.xml:所有的mock类集中于xml中进行管理,然后写了一个JMockitBeanFactory加载这些类。方便以后统计、修改。
-
JMockitBase:所有需要用到mock的单元测试类继承此类,提供getMockBean方法。
AbstractDataProvider:提供从xml读取单元测试源数据入口。
TestNGIInvokedMethodListener:testng中的IInvokedMethodListener监听接口实现,完成MockInfo注解的实现。
注:JMockit是通过类TestNGRunnerDecorator实现Testng的两个接口:IInvokedMethodListener,IExecutionListener来实现和TestNG交互的.。
另外我们mock spring容器中bean的时候,一定要拿到被代理前的原始类。方法如下:
编码实战
-
版本
Testng:6.8
Jmockit:1.21
其他的Spring依赖各位随意吧.
场景一: mock原有的接口返回
笔者这里以金融系统中查询工作日这样的接口举例,通常这种查询我们都会调用一个单独的统一辅助系统去查询某天是否是工作日,然后依次来判断下一步的逻辑。但是笔者希望他永远返回是工作日,且不用配置远程调用的任何信息。(最直接的就是在调用出修改代码,写死返回值,这样虽然可以解决问题但是就像前面笔者说的,复用性不强且容易出问题)
下面笔者列举出Mock主要的代码实现,完整代码笔者会稍后上传到github上。
a. 工作日查询接口
b. 工作日查询接口实现
b. 编写WorkDayAssistant的mock类:
空父类,以后扩展用
Mock类(类的部分mock)(Jmock分类局部mock和全部mock)
基金购买接口
基金购买接口实现
c. 自定义MockInfo注解-用于定义当前test method 运行时需要哪些bean被mock
d. 运行测试
运行结果:
场景二: 从xml获取数据源
对于一些模块和功能已经固化的代码,我们希望用固定的数据在每个迭代版本中都可以得到固定的结果,笔者这里拿金融系统中常见的绑卡场景为例。
a. 定义xml格式
b. DataProvider编写
c. 数据获取使用(这里取数据比较恶心,要从map中get,笔者计划有时间改成JavaBean字段映射)
运行结果:
既然已经取到数据了,那后面的测试和结果校验随意吧。
总结:
-
当前笔者只是简单介绍了框架的简单使用和集成,后续笔者将抽时间将Jmockit的原理、详细使用方法、当期框架设计优化改进的地方再发表出来和大家一起学习交流。
作者:猎狐,就职于开鑫贷,主要负责Java Web方向的开发工作。
官方社区:kplxq.com
开源项目:https://github.com/kplxq/talos
QQ交流群:637375352
微信交流群:添加微信号 qiaojs,并注明“开普勒鑫球”即可