基于 hook 和 gmock 开展单元测试

作者:赵静 团队:腾讯移动品质中心TMQ

一、什么是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大多数情况下也得跟着重新设计,测试后期的维护成本也很高。

获取更多测试干货,关注微信公众号:腾讯移动品质中心TMQ

版权所属,禁止转载。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Go语言实践:从新手入门到上线真实的小型服务所遇到的那些坑

摘要: Teamwork团队在去年写了近20万行Go代码,建造了一堆速度奇快的小型HTTP服务,本文列出了他们总结的9条经验教训。 为什么选择Go语言?Go...

2738
来自专栏逍遥剑客的游戏开发

C#脚本实践(六): 脚本相对于C++的优势

1793
来自专栏大闲人柴毛毛

使用Eclipse插件提高Java编码质量

代码质量概述 ? 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行...

2757
来自专栏CSDN技术头条

和各种诡异 Bug 打交道 13 年,我总结了 18 条经验

作者 | Henrik Warne 翻译 | 郑芸 在《程序员,你会从 Bug 中学习么?》一文中,我写了我是怎样追踪这些年遇到的最有趣 bug 的。最近我重新...

2208
来自专栏java一日一条

怎样编写高质量的Java代码

怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍。也请有过代码...

421
来自专栏哲学驱动设计

分享:使用 TypeScript 编写的 JavaScript 游戏代码

《上篇博客》我写出了我一直期望的 JavaScript 大型程序的开发模式,以及 TS(TypeScript) 的一些优势。博客完成之后,我又花了一天时间试用 ...

2215
来自专栏Golang语言社区

Go语言实践:从新手入门到上线真实的小型服务所遇到的那些坑

摘要: Teamwork团队在去年写了近20万行Go代码,建造了一堆速度奇快的小型HTTP服务,本文列出了他们总结的9条经验教训。 为什么选择Go语言?Go...

3246
来自专栏精讲JAVA

怎样编写高质量的Java代码

代码质量概述 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍...

41510
来自专栏Crossin的编程教室

【我问Crossin】学会 Python 离成为一名程序员还差多远?

1 运行代码时报错:SyntaxError :invalid syntax Crossin: SyntaxError 为语法错误,新手常见的问题可能有: 忘记在...

2575
来自专栏PHP技术

2016最新面试题出炉

小编最近面试了一些公司,有上市公司也有创业公司,但是面试题都大同小异,小编凭记忆汇总了这些公司的面试题,希望对同行业的小伙伴有所帮助。

18510

扫码关注云+社区