白盒测试之Mockito+JMockit+TestNG单元测试实践总结
单元测试实践背景
·测试环境定位bug时,需要测试同学协助手动发起相关业务URL请求,开发进行远程调试
问题:
1、远程调试影响测试环境数据正常获取,影响测试同学测试进度
2、远程调试代码有时并非最新代码,与本地不一致增加调试难度,往往需要发最新的包再调试
3、controller层请求参数依赖特定客户端版本发起,其他版本回归验证,增加模拟操作成本
·依赖第三方系统,第三方系统请求不稳定或希望第三方接口返回特定数据
为什么需要单测
编写单元测试代码并不是一件容易的事情,那为什么还需要去话费时间和精力来编写单元测试呢?
减少Bug:如今的项目大多都是多人分模块协同开发,当各个模块集成时再去发现问题,定位和沟通成本是非常高的,通过单元测试来保证各个模块的正确性,可以尽早的发现问题,而不时等到集成时再发现问题。
放心重构:如今持续型的项目越来越多,代码不断的在变化和重构,通过单元测试,开发可以放心的修改重构代码,减少改代码时心理负担,提高重构的成功率。
改进设计:越是良好设计的代码,一般越容易编写单元测试,多个小的方法的单测一般比大方法(成百上千行代码)的单测代码要简单、要稳定,一个依赖接口的类一般比依赖具体实现的类容易测试,所以在编写单测的过程中,如果发现单测代码非常难写,一般表明被测试的代码包含了太多的依赖或职责,需要反思代码的合理性,进而推进代码设计的优化,形成正向循环。
个人感受,将controller层请求参数抽取管理后,debug不依赖客户端与测试环境,能够迅速在本地执行定位问题;同时,单元测试提供测试数据准备与模拟特定测试数据返回,对业务测试起辅助作用。
单元测试需要理解的几个概念
被测系统:SUT(SystemUnderTest)
被测系统(Systemundertest,SUT)表示正在被测试的系统,目的是测试系统能否正确操作。这一词语常用于软件测试中。软件系统测试的一个特例是对应用软件的测试,称为被测应用程序(applicationundertest,AUT)。
SUT也表明软件已经到了成熟期,因为系统测试在测试周期中是集成测试的后一阶段。
测试替身:TestDouble
在单元测试时,使用TestDouble减少对被测对象的依赖,使得测试更加单一。同时,让测试案例执行的时间更短,运行更加稳定,同时能对SUT内部的输入输出进行验证,让测试更加彻底深入。但是,TestDouble也不是万能的,TestDouble不能被过度使用,因为实际交付的产品是使用实际对象的,过度使用TestDouble会让测试变得越来越脱离实际。
要理解测试替身,需要了解一下DummyObjects、TestStub、TestSpy、FakeObject这几个概念,下面我们对这些概念分别进行说明。
DummyObjects
DummyObjects泛指在测试中必须传入的对象,而传入的这些对象实际上并不会产生任何作用,仅仅是为了能够调用被测对象而必须传入的一个东西。
TestStub
测试桩是用来接受SUT内部的间接输入(indirectinputs),并返回特定的值给SUT。可以理解TestStub是在SUT内部打的一个桩,可以按照我们的要求返回特定的内容给SUT,TestStub的交互完全在SUT内部,因此,它不会返回内容给测试案例,也不会对SUT内部的输入进行验证。
TestSpy
TestSpy像一个间谍,安插在了SUT内部,专门负责将SUT内部的间接输出(indirectoutputs)传到外部。它的特点是将内部的间接输出返回给测试案例,由测试案例进行验证,TestSpy只负责获取内部情报,并把情报发出去,不负责验证情报的正确性。
MockObject
MockObject和TestSpy有类似的地方,它也是安插在SUT内部,获取到SUT内部的间接输出(indirectoutputs),不同的是,MockObject还负责对情报(intelligence)进行验证,总部(外部的测试案例)信任MockObject的验证结果。
FakeObject
经常,我们会把FakeObject和TestStub搞混,因为它们都和外部没有交互,对内部的输入输出也不进行验证。不同的是,FakeObject并不关注SUT内部的间接输入(indirectinputs)或间接输出(indirectoutputs),它仅仅是用来替代一个实际的对象,并且拥有几乎和实际对象一样的功能,保证SUT能够正常工作。实际对象过分依赖外部环境,FakeObject可以减少这样的依赖。
看完TestDouble这几个概念后,是不是一头雾水?以下通俗解释,DummyObjects就不做解释了。
TestStub
系统测试需要某一指定数据返回时,开发将获取数据逻辑代码替换成指定数据,发包测试完再替换回原来逻辑。替换代码返回指定数据,这就是测试桩。
TestSpy
TestStub只返回指定内容给SUT,并没有指定返回测试案例,所以我们引入单元测试,在单元测试用例调用引用该插桩的方法。
这时我们能获测试桩间接输出内容,甚至是报错信息,再也不用到服务器查找错误日志了,这就是TestSpy。
MockObject
MockObject就是在TestSpy的基础上,加入验证机制。调用引用该插桩的方法,我们要确保这个插桩正常被执行或指定执行n次,得到的结果是不是我们期望的结果,mock就以此为生。
FakeObject
FakeObject相对TestStub,是一个面向对象概念。我们只希望替换掉一个实际被引用对象里面的一个方法返回值,被替换某个方法返回值的对象就叫FakeOject,它与实际对象一样的功能。MockObject也囊括FakeObject概念,可以看出TestStub<FakeObject<MockObject。
Mock框架模型
测试验证过程,我们不可能每次都修改代码stub一个方法,发包验证完后再改回,发布外网回归验证阶段这种操作根本不被允许。Mock框架应运而生,我们在单元测试用例stub一个方法后,将之注入被测系统SUT,这个注入只会在testspy阶段产生影响。
市面上很多mock框架,Jmockit、Mockito、PowerMock、EasyMock等,大体遵循record-replay-verify模型设计,有些地方称之为expect-run-verify模式(期望--运行--验证),有些地方称之(AAA阶段)Arrange、Act、Assert,大体一个意思。很明显,Mock框架的应用过程,我们先需要指定stub,然后运行被测方法,然后在验证stub的正确性,这个过程就称之为mock。
单元测试框架选择
Testng
TestNG与Junit很相似,但testng更加灵活,以下为两者对比。
[图片上传失败...(image-93566-1513052813178)]
参考JUnit4VsTestNG比较
·Testng支持分组测试
·Testng参数化测试支持复杂类型参数,而junit只支持基本类型
·Testng提供XML灵活配置测试运行套件
·Testng支持依赖测试
·Testng支持并发测试,上面文章未讲到的,补充下。如@Test(threadPoolSize=3,invocationCount=6,timeout=500),而Junit的话可以引入JunitPref框架。
Jmockit
Jmockit是一个功能很强大的框架,可以mock静态方法、final类、抽象类、接口、构造函数等,几乎无所不能,但编程语言不够简洁。
Jmockit的介绍和使用
这里需要补充的点:
·注解@Tested,标识的被测对象实例,@Injectable的实例会自动注入到@Tested中,有时候在事件过程中实在无法注入,可以借助spring的反射工具ReflectionTestUtils进行注入。
·Expectations:期望,指定的方法必须被调用,且方法默认次数为1。如果指定打桩的方法在test用例不被调用,或者调用次数超过1,则会报错,建议使用NonStrictExpectations配合Verifications使用。
·Expectations(T)/NonStrictExpectations(T),Expectations(.class){}这种方式只会模拟区域中包含的方法,这个类的其它方法将按照正常的业务逻辑运行,T就变成了一个FakeObject。
·MockUp(T)中,未mock的函数不受影响,T也是一个FakeObject。通常rpc接口(接口无具体实现方法)、构造函数通过MockUp进行局部方法mock。
以下主要演示一个rpc接口的mock。
publicclassColumnArticlesControllerTest2extendsBaseContorllerMockTest{
privateMockMvcmockMvc;
@Autowired
privateConfigServiceconfigService;
@Autowired
privateICpDataKievHandlercpDataKievHandler;
@Autowired
privateIndexArticlesDaoCacheImplindexArticlesDao;
@Autowired
privateColumnArticlesControllercolumnArticlesController;
@BeforeMethod()
publicvoidsetUp()throwsException{
mockMvc=MockMvcBuilders.standaloneSetup(columnArticlesController).build();
}
//CSV最好使用gbk格式,目前不支持默认路径,CSV文件位于到dataprovider目录下
@Test(description="测试list.do接口",dataProvider="genData",dataProviderClass=CommonDataProvider.class)
@Csv("/dataprovider/ColumnArticlesControllerTest/testGetColumnArticleList.csv")
publicvoidtestGetColumnArticleList(StringcpChannelId,longcolumnId,StringucParam,Integerv,Stringflymeuid,
Stringnt,Stringvn,Stringdeviceinfo,StringdeviceType,Stringos,IntegersupportSDK,IntegercpType)
throwsException{
Stringimei=deviceinfo.substring(deviceinfo.indexOf("imei="),deviceinfo.indexOf("&"));
ArticleViewparams=newArticleView();
params.setCpChannelId(cpChannelId);
params.setColumnId(columnId);
params.setUcparam(ucParam);
params.setClientReqId(System.currentTimeMillis()+imei);
CommonParamscommonParams=newCommonParams();
commonParams.setV(v);
commonParams.setFlymeuid(flymeuid);
commonParams.setNt(nt);
commonParams.setVn(vn);
commonParams.setDeviceinfo(DeviceUtil.deviceToEncrypt(deviceinfo));
commonParams.setDeviceType(deviceType);
commonParams.setOs(os);
System.out.println(configService.getConfigValue(ConfigKeyEnum.UC_VIDEO_PER));
//jmock静态方法mock掉ip,防止http请求获取Ip报错
newNonStrictExpectations(WebUtils.class,configService){
{
WebUtils.getClientIp();
result="172.17.132.66";
}
{
//后台控制百分比,返回0则过滤掉类型为27的视频,返回100则放开下发该视频“XXX键盘”
configService.getConfigValue(ConfigKeyEnum.UC_VIDEO_PER);
result="100";
}
};
finalICpDataKievHandlercpDataKievHandler2=cpDataKievHandler;
try{
Stringvideo27Articles=FileUtils
.getFileText(FileUtils.getCurrentProjectPath()+"/src/test/resources/afdata/video27Articles.json");
finalCpDataResultvalue=JSON.parseObject(video27Articles,CpDataResult.class);
cpDataKievHandler=newMockUp<ICpDataKievHandler>(){
@mockit.Mock
CpDataResultgetUCArticleList(Stringimei,longchannelId,Stringmethod,Stringrecoid,longftime,
StringcityCode,StringcityName,intpageSize){
returnvalue;
}
}.getMockInstance();
ReflectionTestUtils.setField(indexArticlesDao,"cpDataKievHandler",cpDataKievHandler);
System.out.println(JSON
.toJSON(columnArticlesController.getColumnArticleList(params,supportSDK,cpType,commonParams)));
}finally{
//mock完还原接口方法取值,避免影响其他用例
ReflectionTestUtils.setField(indexArticlesDao,"cpDataKievHandler",cpDataKievHandler2);
}
}