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等。以上是个人拙见,如有不对的地方欢迎大家指正。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 什么是单元测试 在计算机编程中,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最...
    HelloCsl阅读 10,921评论 1 46
  • 1.Creating mock objects 1.1Class mocks idclassMock=OCMCla...
    奔跑的小小鱼阅读 2,567评论 0 0
  • @Author:彭海波 前言 单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小...
    海波笔记阅读 4,930评论 0 52
  • 本文介绍了Android单元测试入门所需了解的内容,包括JUnit、Mockito和PowerMock的使用,怎样...
    于卫国阅读 4,535评论 0 5