内容简介:基于hook和gmock开展单元测试
一、什么是UT
单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等。
对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法—摘自维基百科。
二、为什么要做UT
16年下半年对滴滴SDK接口进行梳理,并进行了BVT接口自动化以及截图半自动化效果验证,但是有几个问题没能得到很好的解决:
(1)SDK的整体代码行覆盖率是57.6%,但导航引擎的覆盖率仅31.2%;
(2)从SDK这层测试导航引擎,需要回放不同类型的轨迹,测试效率低;
(3)从端上直接测试引擎,不符合分层测试思想,较难发现深层次问题。
三、UT开展三部曲
(1)熟悉被测模块
无论是做自动化测试也好,集成测试也罢,都需要对待测模块有一定程度的了解,对于单元测试这种需要深入代码逻辑的测试来讲,更是如此。在开展测试之前,主要从几个方面对待测模块进行分析:代码逻辑、圈复杂度、代码深度、扇入、扇出以及代码行等,如下图1所示:
图1可测性分析
可以看到,该模块有些接口的圈复杂度达到了200+,而业内设计较好的代码圈复杂度在15左右,对这类接口,不建议做UT,最好的方法是让开发进行优化,降低函数的圈复杂度。
(2)选用合适的测试框架
工欲善其事必先利其器,对UT而言也是如此。C++的历史已经非常悠久了,开源框架也是非常多,其中google公司出品的gtest和gmock就是做C++单测的必备神器(https://github.com/google/googletest)。
目前该测试框架可以支持Windows、 Linux 以及Mac OSX平台。
结合SDK实际情况,整合gtest和gmock框架至测试分支,如下图2所示:
图2代码组织结构
这里的UT是嵌入到开发工程里的,做为开发源码WorkSpace中的一个target,该target和之前BVT的target的区别在于,其是基于MAC OSX的Command Line工程,运行环境是MAC OSX,类似于Windows下的可执行文件,而BVT自动化的case运行环境都是基于iOS或者是iOS Simulator系统,这些差别所带来的影响会在第4节中详细说明。
(3)设计单测case
环境部署好了,剩下的就是根据之前的接口分析来设计单测case了。这里举一个简单的例子来进行说明,被测接口是getItem,代码逻辑比较简单,如下图3所示:
图3被测接口
如何设计case呢?对这种既有入参,又有返回值的函数,相对是比较好设计case并进行结果验证的,我们重点关注入参i在不同取值的情况下,函数返回结果是否符合预期。测试代码的编写如下图4:
图4测试用例
这样的case是不是很简单,但在写单测的过程中,我们所面对的测试对象往往复杂的超出你的想象。
四、遇到的问题与解决方案
(1)类的private、protected函数,外部测试类无法调用
开发在设计类时,对于不想让外部类访问的属性以及方法都可以定义为私有的,这并没有什么设计上的问题,但对于测试而言,就要突破这种访问限制,做到public和非public接口都可以在测试类中被访问到,对这个问题,最简洁快速的方法是:在测试类中将private、protected关键字重定义为public,之后在测试类中就可以访问到被测函数的所有方法以及属性。代码如下图5:
图5private可访问
(2)对回调函数的测试
对于C++中的异步回调,可以采用异步变同步的方法,保证该调的时候可以正常的调用。
(3)static以及非虚函数,无法使用现有的框架进行mock
1)为什么无法mock static类型的函数?
在Google Mock的官方“常见问题”的回答中,Google是这样的:You can, but you need to make some changes.即如果你需要mock一个静态函数,那说明你的程序模块过于“紧耦合”了(并且灵活性不够、重用性不够、可测试性不够),你最好是定义一个小接口,通过这个接口来调用那个函数,然后就容易mock了。
2)为什么无法mock非虚函数?
C++ allows a subclass to change the access level of a virtual function in the base class。C++允许用基类的指针来调用子类的函数,举个例子,就很容易明白了,如图6:
图6基类指针调子类函数
非虚函数不具备这样的特性,无法很方便的使用gmock。在实际开发过程中,我们不可能将所有的接口都定义为虚函数,那这个问题如何解呢?
方案一
见 google官方手册https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md,
Google Mock can mock non-virtual functions to be used in what we call hi-perf dependency injection,即依赖注入。该方案的原理是通过模板类的方式来实现,在开发代码中通过传入实际对象来调用真实接口,在测试代码中通过传入mock对象来调用mock出来的接口。Google官方提供的一个例子,如图7:
图7 依赖注入
方案二
重新定义一个mock类B,该类并不继承被测类A,但是在mock类B中,需要实现和A中同样的函数接口,除了待mock的接口。即被测类A和mock类B之间没有任何关系,mock类B中同样实现了被测类A中的大部分接口,在测试代码中,通过声明mock类B的对象,来达到测试目的。
上述两种方案都可以解决gmock不能mock非虚函数的问题,但是都并不完美,均有其缺点:方案一最大的问题是需要修改开发源码,这对于老工程来讲,几乎是不可能的,除非赶上开发重构代码;方案二虽然不会修改开发源码,但是需要维护一套开发代码,当开发代码有变更时,mock的类B需要进行同步修改,无疑加大了测试的维护成本。
如何解决?——Hook
提到hook,就不得不提百度在11年开源的Baiduhook,其提供了linux平台下C/C++程序的hook功能, 可以解决gmock只能mock虚函数的限制。Linux上的hook和windows上的原理差不多,操作基本上是对目标函数进行劫持,替换成自己的函数,然后在自己的函数中进行一些用户预期的操作,比如修改函数返回值等。对hook原理比较感兴趣的可以拜读下源码:https://code.google.com/archive/p/baiduhook/
看起来似乎可以解决我们的问题了,但是不幸的是,目前该hook技术仅支持了Linux平台,而我们的测试框架是在MAC OSX系统下搭建的,MAC OSX是Unix系统,bhook无法在MAC下使用。综合考虑后,决定在Linux系统进行导航引擎的单测。百度以及公司内部都基于hook以及gmock,对gtest进行了二次封装,形成了自己的单元测试框架btest和ttest。
(4)ttest和btest
这两个测试框架的部署,也是废了一番周折……这两个测试框架都依赖Linux的底层系统库libbfd(二进制文件描述库)和libopcodes(程序调试,归档等)。
Øttest:须安装特定版本的binutils以及对应版本的gcc。
1) binutils版本不对
所有的case以及源码编译没有问题,但是在运行case的时候会出现如下图8所示的core:
图8binutils版本错误引起的core
2)gcc版本不对
gcc5.1版本在编译gtest源码库时,会出现链接错误:spec-builders.h:754: undefined reference to `testing::internal::FormatFileLocation
Stack OverFlow上给的解释是:
Øbtest:仍需要特定版本的Linux系统以及gcc版本。
1) 虚拟机centOS4.3+gcc3.4.5
该虚拟机上安装的btest也只有相应的lib和so文件,没有btest的源码,直接运行自带的samples,btest运行完好,没有相应的core。
注:实际运行过程中对gdb版本也有要求(6.7及以上版本),否则会出现:this=dwarf2_read_address: Corrupted DWARF expression。
2) 虚拟机centOS6.5
centOS4.3上整个测试框架运行没有问题,但是毕竟该版本的系统太老了,centOS官方已经停止维护了,各种软件包都没法通过yum来安装,这也给后续配置vim开发环境带来了一定程度的麻烦,所以,就想着能否用高版本的centOS来试下btest是否能运行,结果是不行的,同样会崩到系统库中。
总结,这两个测试框架都是基于Linux系统的hook技术,将hook和gmock完美结合,但是都依赖于Linux系统的底层库,需要特定版本的系统库。虽然有了btest或者ttest,可以很方便的mock接口,但方便的同时,我们就不会再去思考如何对复杂接口进行解耦和了。
(5)有些函数扇出太高,可测性太低
有些历史接口,其扇出达到了40+,代码行也有900+,圈复杂度更是达到了400+,对这样的一类接口,几乎不具可测性,如果这类接口又是业务中很重要的接口,建议开发一起从可测性角度出发重新设计,达到可测性后再来开展单元测试。
五、UT和SDK测试的差异
(1)SDK测试的对象是公开的API,这些API有详细的接口说明文档。UT的测试对象是内部函数,这些函数没有任何文档,需要测试通过debug或者找开发咨询去了解。
(2)SDK测试可能只需要了解某个API被设计来干什么,对其内部如何设计关心的并不多。UT不单需要知道被测函数的功能是什么,还要了解其是如何设计的,实现原理是什么,要求比SDK测试要高。
(3)SDK测试除了要保证接口本身的功能外,更多的还要关心第三方使用者会如何用,即调用场景。UT不需要关心外部如何调,更加聚焦函数本身。
(4)数据构造,UT深入到函数内部,构造的数据不仅仅包含函数入参,还包含函数内部用到的一些数据。
(5)如果代码发生了重构,UT的历史case大多数情况下也得跟着重新设计,测试后期的维护成本也很高。
版权所属,禁止转载
以上所述就是小编给大家介绍的《基于hook和gmock开展单元测试》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 如何开展跨部门协作
- 易鲸捷与Striim开展合作
- 谷歌Chrome开展实验,解决HTTPS混合内容错误
- 联想和微软开展物联网合作 基于Azure平台
- 民族国家黑客团队“海龟”大规模开展DNS攻击
- Intel 公布开源版 Nauta,可开展深度学习模型训练实验
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。