首页 > 软件测试

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

来源:简书

 单元测试实践背景
  · 测试环境定位bug时,需要测试同学协助手动发起相关业务URL请求,开发进行远程调试
  问题:
  1、远程调试影响测试环境数据正常获取,影响测试同学测试进度
  2、远程调试代码有时并非最新代码,与本地不一致增加调试难度,往往需要发最新的包再调试
  3、controller层请求参数依赖特定客户端版本发起,其他版本回归验证,增加模拟操作成本
  · 依赖第三方系统,第三方系统请求不稳定或希望第三方接口返回特定数据
  为什么需要单测
  编写单元测试代码并不是一件容易的事情,那为什么还需要去话费时间和精力来编写单元测试呢?
  减少Bug:如今的项目大多都是多人分模块协同开发,当各个模块集成时再去发现问题,定位和沟通成本是非常高的,通过单元测试来保证各个模块的正确性,可以尽早的发现问题,而不时等到集成时再发现问题。
  放心重构:如今持续型的项目越来越多,代码不断的在变化和重构,通过单元测试,开发可以放心的修改重构代码,减少改代码时心理负担,提高重构的成功率。
  改进设计:越是良好设计的代码,一般越容易编写单元测试,多个小的方法的单测一般比大方法(成百上千行代码)的单测代码要简单、要稳定,一个依赖接口的类一般比依赖具体实现的类容易测试,所以在编写单测的过程中,如果发现单测代码非常难写,一般表明被测试的代码包含了太多的依赖或职责,需要反思代码的合理性,进而推进代码设计的优化,形成正向循环。
  个人感受,将controller层请求参数抽取管理后,debug不依赖客户端与测试环境,能够迅速在本地执行定位问题;同时,单元测试提供测试数据准备与模拟特定测试数据返回,对业务测试起辅助作用。
  单元测试需要理解的几个概念
  被测系统:SUT(System Under Test)
  被测系统(System under test,SUT)表示正在被测试的系统,目的是测试系统能否正确操作。这一词语常用于软件测试中。软件系统测试的一个特例是对应用软件的测试,称为被测应用程序(application under test,AUT)。
  SUT也表明软件已经到了成熟期,因为系统测试在测试周期中是集成测试的后一阶段。
  测试替身:Test Double
  在单元测试时,使用Test Double减少对被测对象的依赖,使得测试更加单一。同时,让测试案例执行的时间更短,运行更加稳定,同时能对SUT内部的输入输出进行验证,让测试更加彻底深入。但是,Test Double也不是万能的,Test Double不能被过度使用,因为实际交付的产品是使用实际对象的,过度使用Test Double会让测试变得越来越脱离实际。
  要理解测试替身,需要了解一下Dummy Objects、Test Stub、Test Spy、Fake Object 这几个概念,下面我们对这些概念分别进行说明。
  Dummy Objects
  Dummy Objects泛指在测试中必须传入的对象,而传入的这些对象实际上并不会产生任何作用,仅仅是为了能够调用被测对象而必须传入的一个东西。
  Test Stub
  测试桩是用来接受SUT内部的间接输入(indirect inputs),并返回特定的值给SUT。可以理解Test Stub是在SUT内部打的一个桩,可以按照我们的要求返回特定的内容给SUT,Test Stub的交互完全在SUT内部,因此,它不会返回内容给测试案例,也不会对SUT内部的输入进行验证。
  Test Spy
  Test Spy像一个间谍,安插在了SUT内部,专门负责将SUT内部的间接输出(indirect outputs)传到外部。它的特点是将内部的间接输出返回给测试案例,由测试案例进行验证,Test Spy只负责获取内部情报,并把情报发出去,不负责验证情报的正确性。
  Mock Object
  Mock Object和Test Spy有类似的地方,它也是安插在SUT内部,获取到SUT内部的间接输出(indirect outputs),不同的是,Mock Object还负责对情报(intelligence)进行验证,总部(外部的测试案例)信任Mock Object的验证结果。
  Fake Object
  经常,我们会把Fake Object和Test Stub搞混,因为它们都和外部没有交互,对内部的输入输出也不进行验证。不同的是,Fake Object并不关注SUT内部的间接输入(indirect inputs)或间接输出(indirect outputs),它仅仅是用来替代一个实际的对象,并且拥有几乎和实际对象一样的功能,保证SUT能够正常工作。实际对象过分依赖外部环境,Fake Object可以减少这样的依赖。
  看完Test Double这几个概念后,是不是一头雾水?以下通俗解释,Dummy Objects就不做解释了。
  Test Stub
  系统测试需要某一指定数据返回时,开发将获取数据逻辑代码替换成指定数据,发包测试完再替换回原来逻辑。替换代码返回指定数据,这就是测试桩。
  Test Spy
  Test Stub只返回指定内容给SUT,并没有指定返回测试案例,所以我们引入单元测试,在单元测试用例调用引用该插桩的方法。
  这时我们能获测试桩间接输出内容,甚至是报错信息,再也不用到服务器查找错误日志了,这就是Test Spy。
  Mock Object
  Mock Object就是在Test Spy的基础上,加入验证机制。调用引用该插桩的方法,我们要确保这个插桩正常被执行或指定执行n次,得到的结果是不是我们期望的结果,mock就以此为生。
  Fake Object
  Fake Object相对Test Stub,是一个面向对象概念。我们只希望替换掉一个实际被引用对象里面的一个方法返回值,被替换某个方法返回值的对象就叫Fake Oject,它与实际对象一样的功能。Mock Object也囊括Fake Object概念,可以看出Test Stub < Fake Object < Mock Object。
  Mock框架模型
  测试验证过程,我们不可能每次都修改代码stub一个方法,发包验证完后再改回,发布外网回归验证阶段这种操作根本不被允许。Mock框架应运而生,我们在单元测试用例stub一个方法后,将之注入被测系统SUT,这个注入只会在test spy阶段产生影响。
  市面上很多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)]
  参考 JUnit 4 Vs TestNG比较
  · 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就变成了一个Fake Object。
  · MockUp(T)中,未mock的函数不受影响,T也是一个Fake Object。通常rpc接口(接口无具体实现方法)、构造函数通过MockUp进行局部方法mock。
  以下主要演示一个rpc接口的mock。
public class ColumnArticlesControllerTest2 extends BaseContorllerMockTest {
private MockMvc mockMvc;
@Autowired
private ConfigService configService;
@Autowired
private ICpDataKievHandler cpDataKievHandler;
@Autowired
private IndexArticlesDaoCacheImpl indexArticlesDao;
@Autowired
private ColumnArticlesController columnArticlesController;
@BeforeMethod()
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(columnArticlesController).build();
}
// CSV最好使用gbk格式,目前不支持默认路径,CSV文件位于到dataprovider目录下
@Test(description = "测试list.do接口", dataProvider = "genData", dataProviderClass = CommonDataProvider.class)
@Csv("/dataprovider/ColumnArticlesControllerTest/testGetColumnArticleList.csv")
public void testGetColumnArticleList(String cpChannelId, long columnId, String ucParam, Integer v, String flymeuid,
String nt, String vn, String deviceinfo, String deviceType, String os, Integer supportSDK, Integer cpType)
throws Exception {
String imei = deviceinfo.substring(deviceinfo.indexOf("imei="), deviceinfo.indexOf("&"));
ArticleView params = new ArticleView();
params.setCpChannelId(cpChannelId);
params.setColumnId(columnId);
params.setUcparam(ucParam);
params.setClientReqId(System.currentTimeMillis() + imei);
CommonParams commonParams = new CommonParams();
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报错
new NonStrictExpectations(WebUtils.class, configService) {
{
WebUtils.getClientIp();
result = "172.17.132.66";
}
{
// 后台控制百分比,返回0则过滤掉类型为27的视频,返回100则放开下发该视频“XXX键盘”
configService.getConfigValue(ConfigKeyEnum.UC_VIDEO_PER);
result = "100";
}
};
final ICpDataKievHandler cpDataKievHandler2 = cpDataKievHandler;
try {
String video27Articles = FileUtils
.getFileText(FileUtils.getCurrentProjectPath() + "/src/test/resources/afdata/video27Articles.json");
final CpDataResult value = JSON.parseObject(video27Articles, CpDataResult.class);
cpDataKievHandler = new MockUp<ICpDataKievHandler>() {
@mockit.Mock
CpDataResult getUCArticleList(String imei, long channelId, String method, String recoid, long ftime,
String cityCode, String cityName, int pageSize) {
return value;
}
}.getMockInstance();
ReflectionTestUtils.setField(indexArticlesDao, "cpDataKievHandler", cpDataKievHandler);
System.out.println(JSON
.toJSON(columnArticlesController.getColumnArticleList(params, supportSDK, cpType, commonParams)));
} finally {
//mock完还原接口方法取值,避免影响其他用例
ReflectionTestUtils.setField(indexArticlesDao, "cpDataKievHandler", cpDataKievHandler2);
}
}
Mockito
  Mockito区别于其他模拟框架的地方允许开发者在没有建立“预期”时验证被测系统的行为,编码设计简洁优美,使用简单快捷,成本低。同时Mockito提供@Spy注解实例,这个注解是将实例对象的指定方法返回值给stub掉,而不是将方法内部处理逻辑给跳过。注意,@Spy监视的是一个真实对象。@Spy录制期望,调用真实的方法,这个对我们测试来说很重要,因为这样我们才能保证对stub方法输入的合理性,对stub方法内部调用正确性,Mockito的@Mock注解包括前的JMockit对一个对象的Mock,都是直接跳过调用真实方法而返回录制期望值,如果没录制则返回null,而@Spy对未stub的方法,返回真实的调用逻辑值。
  Mockito的缺点是不能stub静态方法、final类、构造函数、匿名类,所以最好配合Jmockit使用。
  学习参考 Mockito 初探
  允许开发者在没有建立“预期”时验证被测系统的行为,如下实例不建立期望,只验证交互
// 模拟的创建,对接口进行模拟
List mockedList = mock(List.class);
// 使用模拟对象
mockedList.add("one");
mockedList.clear();
// 选择性地和显式地验证
verify(mockedList).add("one");
verify(mockedList).clear();
与spring组合的简单示例:
public class SearchControllerTest extends BaseContorllerMockTest {
private MockMvc mockMvc;
private static final Logger ILOG = LoggerFactory.getLogger(SearchControllerTest.class);
@Autowired
private IRedisClient redisClient;
@Spy
@Autowired
private SearchService searchService;
@InjectMocks
@Autowired
private SearchController searchController;
@BeforeMethod()
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(searchController).build();
MockitoAnnotations.initMocks(this);
}
@Test
public void testGetHotWords(){
Mockito.when(searchService.getHotWords(Mockito.anyInt(), Mockito.anyInt())).thenReturn(Arrays.asList("周杰伦","林俊杰"));
System.out.println(JSON.toJSON(searchController.getHotWords(0, 30)));//输出{"value":{"words":["周杰伦","林俊杰"]},"message":"","redirect":"","code":200}
}
}
  MockMvc
  相信眼尖的你通过上面的示例发现了MockMvc,参考学习 SpringMVC 测试 mockMVC
  为什么使用MockMvc呢?
  · 从学习参考示例看MockMvc URL调用是不是很贴近接口自动化,MockMvc让我们能测试完整的Spring MVC流程。我们前面的mock示例中直接调用controller层方法要自行构建参数,得到的函数方法结果要经过fastjson进行转换才是是最终下发给客户端的结果,这中间其实绕过了spring mvc拦截器和转换器,通过MockMvc就跟模拟接口请求一样,请求经过拦截器验证、参数自行绑定与转换等。
  · MockMvc提供诸如MockHttpServletRequest、MockHttpServletResponse、MockHttpSession重量级对象mock,分别对应HttpServletRequest、HttpServletResponse、HttpSession。
  下面示例restful结果通过MockHttpServletResponse输出,即是返回给客户端的最终结果。
@Test(description = "头条get.do接口,通过模拟请求链接")
public void testGetMethodThroughMockRequestUrl() throws Exception {
MvcResult result = mockMvc
.perform(get("/android/unauth/settings/get.do").param("v", "3021000").param("flymeuid", "113516747")
.param("nt", "wifi").param("deviceType", "mx5").param("os", "5.1-1505319080_stable")
.param("vn", "3.21.0").param("deviceinfo",
"v6FBm9zBUDEtahUN942%2Fyg9SrkQPmTvaFwvgfujjfk%2BxjcNQL0fr1Knx9TMeqzZVAQVBqkdzfe9b9ZM8P2p%2BucjGohlhGn0MvEKrSJ1XbUYOEBTUJG%2Bjvvf1c2v0qXhfqkx37mT%2Ffii1KgiQ6zGNhOLjjN9QxC1Lsx2D6jDPqcQ%3D"))
.andReturn();
MockHttpServletResponse mockHttpServletResponse = result.getResponse();
String s = mockHttpServletResponse.getContentAsString();
System.out.println(s);
}
  纸上得来终觉浅,觉知此事要躬行,在实践过程中总会发现很多跟网上教案冲突的地方,这时候就要多尝试多思考多验证。这里只介绍了单元测试的冰山一角,单元测试还有PowerMock、DbUnit等。以上是个人拙见,如有不对的地方欢迎大家指正。
 

软件测试

  

   

投稿关闭窗口打印