当前位置: 首页 > 科技观察

C-C++单元自动化测试解决方案实践

时间:2023-03-12 06:26:05 科技观察

作者|vivo互联网服务器团队-李庆新C/C++开发效率一直被业界开发者诟病,单元测试开发效率也是如此,让开发者舍不得花钱时间编写单元测试。那么是否可以通过提高编写单元测试的效率来提高项目的测试用例覆盖率呢?本文主要介绍如何使用GCC插件为C/C++开发者实现单元效率工具解决方案。希望大家可以完善单元测试。灵感来自效率。一、动机上图展示了C/C++单元测试的基本流程。在日常开发过程中编写单元测试是一个比较大的工作量。目前C/C++的单元测试代码需要自己手工编写,对于一些Private方法的堆砌更是麻烦。目前业界还没有开源的自动化测试框架或工具,但有一些商业化的自动化测试工具。下图是我们的自动化测试工具和单元测试库:即使开源社区有gtest等测试库的支持,我们仍然需要编写大量的单元测试用例代码。对于一些private和protected类的方法,编写单元测试用例的效率就更低了,需要手工打桩(mock)。同时,我们对测试用例进行分析,发现有很多边界用例,基本上是固定的或者有一定的模式,比如int的最大值和最小值。如何提高编写单元测试的效率,提高C/C++同学的开发效率和程序质量?我们可以提取源文件中的函数、类等信息,然后生成相应的单元测试用例。在自动生成用例时,我们需要依赖函数声明、类声明等信息,那么我们应该如何获取这些信息呢?例如:下面的函数定义:voidtest(intarg){}我们希望能够从上面的函数定义中得到函数返回值类型、函数名、函数参数类型、函数作用域。通常我们可以通过以下几种方式获取:1.1方法一:使用正则表达式,但是C/C++格式比较复杂,虽然可以使用多种组合来获取对应的函数声明等信息:voidtest(intarg){}voidtest1(template>arg,...){}voidtest2(int(*func)(int,float,...),template>arg2){}然后需要写一系列的正则表达式:提取函数名,参数名:[z-aA-Z_][0-9]+提取函数返回值:^[a-zA-Z_]提取关键字,但是他有一个很大的疑问:如何判断文件中编写的代码是否符合C/C++语法描述?1.2方法二:使用flex/bison分析C/C++源代码文件。这当然是个好办法,但工作量巨大。它相当于实现了一个简单版本的词法和语法分析器的编译器,它需要适应不同的语法格式。bison虽然可以解决上面提到的如何判断语法是否正确的问题,但是还是很复杂。1.3方法三:通过编译生成的AST生成代码。通常我们知道GCC的编译过程是以下四个阶段:源文件->预处理->编译->汇编→链接,但实际上GCC支持更多,针对不同的编程语言和不同的CPU架构做了很多优化,如下图所示:上图为GCC处理源码等优化过程。与源语言无关的抽象语法表示(AST)。由于AST树是在GCC编译过程中生成的,我们可以使用GCC插件提取GCC前端生成的抽象语法树的关键信息,如函数返回值、函数名、参数类型等.整体难度也很高。一方面,业内参考资料少。我们只能通过分析GCC的源码来分析AST语法树上各个节点的描述。本文介绍的自动生成单元测试用例的方案(我们称之为TU:TranslateUnit,以下统称TU)是基于方法三,先来看看我们的自动化测试用例方案。显示结果。2、效果展示2.1业务代码零修改,直接使用TU生成边界用例。在这个用例中,我们可以在不修改任何业务代码的情况下,为业务代码生成边界测试用例,并且函数参数可以用边界值进行充分的排列,大大降低了用例遗漏的风险。您可能会发现这个用例中没有任何断言是未经任何修改而生成的。虽然没有断言,但还是可以帮助找出单元中是否存在边界值导致coredump。那么如果要给他加上断言和mock函数,就没有办法了吗?通过C++11新的属性语法[[]],只需要在声明或定义方法时按照TU的格式添加断言即可,不侵入业务逻辑。2.2使用注解tu::case生成用户自定义测试用例很多时候默认生成的边界测试用例无法覆盖核心逻辑,所以我们也提供了tu::case让用户自定义自己的测试用例和断言。比如有一个intfoo(intx,longy)方法,现在要添加一个返回值为123,函数参数为1,1000的测试用例,那么在函数声明之前添加即可,[[tu::case("NE","123","1","1000")]]2.3使用注解tu::mock自动生成mock方法在开发过程中,我们经常需要mock一个方法(即在原来的方法中设置一个临时的替换方法,并保持调用方法一致),比如函数访问Redis和DB时,单元测试往往需要mock这些方法,方便单元测试其他函数调用。为了方便单元测试,我们经常会被mock,所以为了方便开发者快速mock,我们提供了tu::mock注解帮助开发者快速定义注解,然后TU会自动生成对应的mock函数。例如:现在mock一个foo_read方法的函数,让mock函数返回10:3.TU实现方案3.1什么是AST?GENERIC、GIMPLE和RTL构成了整个gcc中间语言,以GIMPLE为核心,而GENERIC继承自RTL,在源文件和目标指令之间的缝隙上建立了三层过渡。在GCC的语法分析过程中,所有被识别的语言成分都存储在一个名为TREE的变量中。这个TREE就是GCC语法树(AST),这个过程叫做GENERIC。其实它也是GCC的符号表,因为变量名、类型等信息都是通过TREE关联起来的。我们通过gcc的编译选项来看一下gcc的ast表示:3.2AST(抽象语法树)GCC可以通过添加编译选项-fdump-tree-all来生成ast树。ast树文件内容如下:每一种AST都可以描述参考:https://gcc.gnu.org/onlinedocs/gccint/Types.html虽然简单看一下上图,还是有的节点之间的依赖关系以gcc的形式存在,难以理解。没有clang产生的直观更容易阅读。虽然不利于阅读,但不影响通过编码提取AST信息。3.3方案如上图所示,我们使用不同的插件收集被测源文件的AST信息、头文件信息、函数注解(属性),并保存这些重要信息。GCC将用户注册插件事件保存到一个数组中:然后在编译构建过程中会去寻找对应的事件。如果设置了回调方法,它将被调用。TU主要使用以下插件:PLUGIN_INCLUDE_FILE用于获取当前文件包含的头文件PLUGIN_OVERRIDE_GATE用于用户获取普通功能,类PLUGIN_PRE_GENERICIZE用于获取模板功能的实现。PLUGIN_ATTRIBUTES用于实现自定义属性或注解(tu::case\tu::mock....)GCC支持的所有插件类型如下图所示:(摘自gcc6.3.0源码)4.TU插件易用性对比如果只是做边界测试,只需要修改cmake等构建脚本,添加相应的插件参数即可。5、使用TU的优点是接入简单,边界单元测试可以实现业务代码0的全编排,修改函数参数,边界值,大大降低漏用例的风险,减少大量重复性工作。快速生成自定义用例和mock方法等6.TU支持的功能7.总结与展望1.文章比较了三种自动生成测试用例的方法。下面对这些方法进行比较:2.文章还主要介绍了TU的功能特点以及一种基于GCC-AST自动生成测试用例的解决方案。目前TU解决方案可以在构建过程中自动生成测试用例,大大降低了单元测试的门槛,提高了单元测试的覆盖率。未来,我们也希望将TU与IDE相结合,探索更高效便捷的使用方式。为指定方法生成测试用例的方法。比如通过函数和方法上的快捷键为当前方法生成测试用例。