白盒测试之Mockito+JMockit+TestNG单元测试实践总结

单元测试实践背景

·测试环境定位bug时,需要测试同学协助手动发起相关业务URL请求,开发进行远程调试

问题:

1、远程调试影响测试环境数据正常获取,影响测试同学测试进度

2、远程调试代码有时并非最新代码,与本地不一致增加调试难度,往往需要发最新的包再调试

3、controller层请求参数依赖特定客户端版本发起,其他版本回归验证,增加模拟操作成本

·依赖第三方系统,第三方系统请求不稳定或希望第三方接口返回特定数据

白盒测试之Mockito+JMockit+TestNG单元测试实践总结

为什么需要单测

编写单元测试代码并不是一件容易的事情,那为什么还需要去话费时间和精力来编写单元测试呢?

减少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);

}

}

相关推荐