点击「京东金融技术说」可快速关注
「引言」之前做单元测试,使用过很多单元测试的mock框架,但都觉得不好用,所以自己开发了一个框架—pmock,无侵入,零学习成本,且能和已有的单元测试基本无缝集成。让对单元测试退避三舍的童鞋觉得零负担。本文分为起承转合四个篇幅介绍这个框架:
起的篇讲讲单元测试
承的篇讲各种已有mock框架进行对比
转合两篇讲新框架的使用、配置、原理
对单元测试和mock测试掌握比较深入的童鞋,可以直接跳到转合篇看。或者直接看源码:
内网:http://source.jd.com/app/pmock.git
github:https://github.com/yangtaihsou/pmock——单元测试类PmockTest.java。
起
一、为什么要做单元测试?
工作多年,经历了很多bug,感觉大部分bug基本都是逻辑不严密或者粗心导致。系统简单还好说,如果复杂,大部分时间花在环境启动集成连通上,这时,bug造成的线上问题,工单、客诉,影响非常大。并且后续善后也非常耗费时间,或有可能因处理BUG产生其他问题。
开发流程中能解决上述问题的,我觉得最重要的是单元测试。当某块程序写完后,case覆盖率全面的单元测试,就是一个bug扫雷器,具有强大的侦查能力,以前需要debug几个小时,现在几分钟就能找到。当所有程序写完,通过testNG将相应的所有单元测试组合在一起,一键做回归测试,不用再害怕修复bug引起别的bug。
目前情况,开发完毕测试直接进行集成和连通性测试;或者等着依赖的模块完成,进行自测。现在的系统都非常复杂,如果这样测试,可能产生各种各样的问题。
二、什么是单元测试?
单元测试是指对软件中的最小可测试单元进行检查和验证,准确、快速地保证程序基本模块的正确性,主要是开发人员或测试研发发起。在java系统中,最小指一个方法的测试。单元测试好处多多。
通过先测试最小模块,保证最小模块的质量来最大保证系统质量;
可以倒逼对程序的抽象、重构,达到“眼中无码、心中有码”的境界;
能更好理解TDD(测试驱动开发)、BDD(行为驱动开发);
积累一定的单元测试训练后,即使直接开发代码,也能更好做到代码的提炼和抽象 。
承
一、单元测试示例和问题
先看看要进行单元测试的类PersonBusinessServiceImpl和被测试方法queryStudents。queryStudents方法是从不同性别的人员中,挑选出学生,返回去。
下图大红框里是逻辑,是要覆盖测试的;小框里是测试的queryStudents依赖的数据和对象,是personBusinessDao的queryPersonList,也是重点要造的数据。(这个方法的逻辑和依赖比较简单)
1、下图单元测试,从男性人员列表中,筛选出学生列表。即对PersonBusinessServiceImpl的queryStudents方法进行单元测试。setUp里对queryStudents方法里依赖的对象dao进行赋值。
问题:setUp创建的dao,如它里面依赖的东西路径很多很深,需要层层进行new对象赋值,操作麻烦,会造成setUp很多。而且依赖personBusinessDao的queryPersonList返回的数据,提前无法确定,程序运行时才知道。
2、再看看自测的单元测试(用testNG代替junit)。下图,使用spring的依赖注入,代码简单。
问题:实际上这个不是单元测试,而是集成测试、连通测试。queryStudents方法里可能调用很多外部系统,比如数据库。且这种配置直接将整个spring容器启动,很耗时,如系统复杂更耗时(我开发了一个插件,可以只加载特定的bean,快速启动spring容器)。
3、能不能对queryStudents方法里依赖的对象,进行简单赋值?可以,使用mock框架测试,前面的单元测试鸡汤终于给勺子了。
mock测试定义:对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。实际在分布式系统里,不容易获取的对象不是某些,是全部。
下图是mockito框架的示例。图中红框里,通过mockito框架,mock了一个PersonBusinessDao对象和对象方法queryPersonList的返回数据(没有设定queryPersonList的输入参数)。
mock的关键代码:有when、thenReturn。还有其他的一些函数,如下
mockito相比easymock框架已经算比较好用的mock框架,下面再看一个更好用的。
4、groovy语言的spock框架,在jvm上运行,where的参数设定甩了其他框架几条街。这个框架充分利用了groovy的动态语言特性,书写很方便,也兼容java的书写风格,通过不同block块让代码简单清晰,可以BDD开发(就是先写每个block块的详细描述,再去开发实际代码),有given、and、when、then、where。
二、几款mock框架对比
spock和其他mock框架的对比如下
mock框架优点:
模拟资源
隔离系统或模块
并行开发
TDD模式开发
快速演示
覆盖度广
mock框架缺点:
需要很多硬编码。因为测试覆盖度广,很多不同的case数据需要硬编码
侵入性强。好用的spock框架,很难和spring风格的单元测试结合起来
应对需求不足。需求变化时,单元测试和case难以维护
学习成本高
转
看了不少单元测试的书和文章,感觉里面大量的测试case和覆盖率让人负担很重。虽然我曾为一个模块花了整整两天时间,编写大量的单元测试,运行他们的那一刻,获得了深深的满足感;也节约了大量测试时间,面对复杂的逻辑和分支流程,添加几个case运行即可。并且,在变更需求加逻辑和分支的过程中,这些写好的单元测试仍然能发挥很好作用。但写单元测试确实非常耗时。同时,我也不认同测试case覆盖率:测试你认为容易出错的地方。实际上,程序员最容易阴沟里翻船,在认为最保险的地方出错。所以就有了开发一款mock框架的初衷。
在我心目中,mock测试应该无侵入式,0学习成本,单纯保存case的输入参数和输出参数就好了。所以,按照这个产品思路,制作了一个mock框架,先取名pmock。
案例:按照习惯用spring集成unit进行测试的风格,只要在vm参数添加
-javaagent:realpathpmock-agent.jar即可无侵入进行mock测试(后面有示例设置javaagent)。
具体怎么实现的,先讲讲上面提到的产品思路:单纯保存case的输入参数和输出参数。如下图示意,只关心mock对象(可以是内部或者第三方的接口)的方法queryPersonList,在不同的输入参数,响应不同的返回参数。
以下示例图是更直观的影响,图中是针对不同入参的返回,可以随意编写cese。
具体如何使用,以下是配置介绍,需要依赖pmock-agent.jar包和case配置
1、首先需要本地配置case和初始化
在自己工程下的测试(test)目录下资源(resources)目录,添加pmock文件夹。
pmock文件下有initConfig.properties文件,里面的属性loadSource=local,表示告诉pmock从本地硬盘也就是caseConfig目录下读取case文件;如果loadSource=net,表示告诉pmock从pmockserver中心服务器,拉取case配置。
caseConfig下,每个被mock的类,可以由你创建一个groovy脚本文件,并且根据测试需要,创建mock方法,方法内是不同的case数据,也就是一堆if else。以下是详细示意图。如果不确定脚本写的是否有问题,可以在main函数里进行编写测试。
2、那么mock是如何执行这些case?
首先是无侵入式的mock,通过-javaagent:realpath\pmock-agent.jar进行配置。主要适用于被mock的对象是类,不是接口。idea里的配置示意图。至于eclipse、tomcat如何设置javaagent可以自行解决。
前面提过spring的单元测试,可以无缝使用pmock。但做真正的单元测试,是不会启动非常耗时的spring容器的。以下图示意,这时注销头两行的spring启动,测试类=null需要制造测试类对象,这时加上javaagent就可以正常运行了(文章最后,我会演示如何不用注销也不会启动spring容器的)
接口的mock创建。如果被mock的对象是接口,是不能javaagent的,需要硬编码(但算不上侵入,如上图那样显式声明对象而已)。很简洁,最大兼容了其他mock框架的函数式编程风格,且非常灵活。
mockTarget、mockOject、mockField、target让mock一目了然,可以连续mock多个对象。注意:mockTarget和mockOject位置可以互换,但mockOject和mockField最好成对出现,且每对里的mockOject在mockField前面;或者可以类似这样:
另外:当然如果觉得javaagent麻烦,也可以对类进行mock。mockObject(PersonBusinessDao.class)换成mockObject(PersonBusinessDaoImpl.class)就好。
注意:mock接口对象,就要创建接口名的groovy文件case,mock类对象,就要创建类名的groovy文件case。
继续看不同的mock创建风格(这个示例可以直接看代码)。红框里极简式mock风格,完毕后,被测试类的实例可以继续使用。
如果不想使用函数式风格,可以继续用传统严肃的风格,图中两种。
注意:直接使用方法,是因为类被静态引入import static com.jd.jr.pmock.agent.Pmock.*;
合
更多示例可以参考源码。这章讲讲产品实现的技术栈和原理。里面主要用到了javassist、fastjson、groovy。查看源码的pmock-agent工程,只依赖这3个:
接口mock对象的产生,主要原理就是进行代理。javassist进行接口或者类的代理。另外完全无侵入式通过javaagent实现,里面主要对需要mock的类(接口不行)进行方法植入代码。javaagent的思路主要参考了京东金融apm产品sgm的实现思路。
fastjson方便快速序列化成对象,比gsoon好用。
groovy用来做case编写。使用groovy是因为第一版风控规则就是动态执行groovy脚本,也因为对spock框架的惊艳,groovy脚本学些成本非常低。注意:如果执行groovy脚本里main测试好使,执行mock却不好使,记得按照java风格加标点符号即可。
遇到的坑:使用4天时间开发,一半时间花在了javassist的代理实现上;javassist下的类,进行代理操作,会擦掉泛型,没法使用,不断试版本也不行。最后逛官网文档,发现javassist.util.proxy下的ProxyObject很好使,操作简单,比jdkproxy代理接口更简洁。可以看源码JavassistHelper实现了多种代理。一开始就应该对接口进行jdkproxy代理,对类使用javassist植入方法代码的代理方式,可能速度更快点。对类使用javassist植入方法代码的代理方式,已经实现,但是破坏了加载器的双亲委托机制,怕有潜在风险。也可以使用别的如cglib进行代理,可以更方便点。javassist文档:
http://jboss-javassist.github.io/javassist/html/overview-summary.html
遗留问题:
pmock如何和自动化测试结合起来,是下一步需要考虑的
如何像spock那样方便的设置输入参数和响应数据。
最后,单元测试虽好,但集成测试和连通测试也是不能替代的,甚至有时集成测试的case应该比单元测试更多。
番外
一、如何将spring和pmock很好的结合起来。如图,在setUp里,进行开关配置判断,如果走spring测试,通过编码启动spring容器,不是注解方式。
二、为了真实验证rpc接口,类似http请求,做了PlayRpc接口示例,并使用了jdkproxy进行代理,并且可以通过spring配置注入。可以看看jdkproxy的接口代理实现,和javassist的ProxyObject对比下,确实麻烦些。
三、前文提到,直接加载整个spring容器非常耗时,如果真要启动spring容器,可以精简启动的xml配置。ScanDependencyBeanUtil.java这是我之前写插件,扫描被测试类要依赖的注入对象,将依赖的注入单独配置xml,通过上图ClassPathXmlApplicationContext进行变动启动spring容器。这个插件也可以用来检测被测试类的简洁性,如果依赖东西太多,可以考虑重构。
四、pmockserver。case的脚本从本地放在配置中心,让工程里的测试代码更加简单。支持使用groovy、js、python、ruby等多个脚本语言配置case。在工程里设置好要从local还是net取case执行就可以了,且设好pmockserver的url地址。pmockserver测试地址:172.25.35.164 pmock.jd.com
如感兴趣可以咚咚联系作者,欢迎沟通交流~
京东金融技术说
▼▼▼
原创·实用·技术·专业
不只一技之长
我有N技在手
你看,我写,共成长!