共计 3424 个字符,预计需要花费 9 分钟才能阅读完成。
vivo 互联网服务器团队 – Li Qingxin
C/C++ 开发效率始终被业内开发人员诟病,单元测试开发效率也是如此,以至于开发人员不愿花工夫来写单元测试。那么咱们是不是能够通过改善编写单元测试的效率来晋升我的项目的测试用例覆盖率?
本文次要介绍如何利用 GCC 插件来实现晋升 C /C++ 开发者的单元效率工具解决方案,心愿对大家在晋升单元测试效率上有所启发。
一、动机
上图展现了 C /C++ 单元测试的根本流程,在日常开发过程中写单元测试是一项比拟大工程量的事件,C/C++ 目前单元测试代码都须要本人手动写,而且对于一些公有办法打桩就更加麻烦。
目前业内无开源的自动化测试框架或者工具,倒是有一些商业的主动测试工具,下图展现了咱们自动化测试工具及单元测试库:
即便开源界有 gtest 等测试库的反对,咱们依然须要编写大量的单元测试用例代码。对于一些 private、protected 的类办法,编写单元测试用例的效率就更低,须要手动打桩(mock)。同时咱们剖析测试用例发现,存在很多边界的用例,它们基本上都是很固定或者有肯定模式,比方 int 最大最小值等。
如何改善编写单元测试的效率,晋升 C /C++ 同学开发效率以及程序品质?咱们能够通过提取源文件中的函数、类等信息,而后生成对应的单元测试用例。主动生成用例时须要依赖函数的申明、类的申明等信息,那么咱们应该如何获取这些信息呢?
例如:如下的函数定义:
void test(int arg) {}
咱们心愿可能从下面的函数定义中失去函数的返回值类型、函数名称、函数参数类型、函数作用域。通常咱们能够通过以下几种形式失去:
1.1 办法 1:应用正则表达式
无奈 C /C++ 格局比较复杂可能尽管可能应用多种组合来获取对应的函数申明等信息:
void test(int arg){}
void test1(template<template<string>> arg,...){}
void test2(int(*func)(int ,float,...),template<template<string>> arg2){}
那么就须要写一系列的正则表达式:
- 提取函数名称、参数名:[z-aA-Z_][0-9]+
- 提取函数返回值:^[a-zA-Z_]
关键词提取进去了,然而他有一个很大的问题:怎么判断文件中书写的代码是合乎 C /C++ 语法形容呢?
1.2 办法 2:应用 flex/bison 剖析 c /c++ 源码文件
这当然是一种很好的形式,然而工作量微小,相当于实现一个具备词法、语法分析器繁难版本的编译器,而且要适配不同的语法格局,尽管 bison 能够解决上述的如何判断语法是否正确问题,然而依然很简单。
1.3 办法 3:利用编译曾经生成的 AST 来生成代码
通常咱们理解到的 GCC 编译的过程是以下四个阶段:
源文件 -> 预处理 -> 编译 -> 汇编→链接
但实际上 GCC 为了反对更多的编程语言、不同的 CPU 架构做了很多的优化,如下图所示:
上图展现了 GCC 解决源码及其他优化过程,在前端局部生成的 Generic 语言是 gcc 编译过程中为源码生成的一种与源码语言无关的形象语法表现形式(AST)。既然 GCC 编译过程中生成了 AST 树,那么咱们能够通过 GCC 插件来提取 GCC 前端生成的形象语法树要害信息比方函数返回值、函数名称、参数类型等。总体难度也很高,一方面业内可参考资料很少,只能通过剖析 GCC 的源码来剖析 AST 语法树上的各个节点形容。
本文所形容的自动化生成单元测试用例的解决方案(咱们称之为 TU:Translate Unit,后文统称为 TU)就是基于办法 3 来实现的,上面咱们先来看看咱们的自动化测试用例解决方案的成果展现。
二、成果展现
2.1 业务代码零批改,间接应用 TU 生成边界用例
在该用例中咱们不须要批改任何业务代码就可能为业务代码生成边界测试用例,而且函数参数可边界值实现全排列,大大降低用例脱漏危险。大家可能发现这种没有做任何批改生成的用例是没有断言的,尽管没有断言,它依然可能帮忙发现单元是否会存在边界值引起 coredump。
那么如果想要给他加上断言、mock 函数,是否没有方法呢?通过 C ++11 [[]] 新的属性语法,只须要在办法申明或者定义时增加下依据 TU 的格局增加断言即可,对业务逻辑无侵入。
2.2 应用注解 tu::case 生成用户自定义用例
很多状况下默认生成的边界测试用例还不能笼罩到外围逻辑,所以咱们也提供 tu::case 来给用户自定义本人的测试用例及断言。比方有一个 int foo(int x,long y) 办法,当初想新增一个测试用例返回值 123,函数实参 1,1000,那么只有在函数申明前退出,以下代码即可:
[[tu::case(“NE”,”123″,”1″,”1000″)]]
2.3 应用注解 tu::mock 主动生成 mock 办法
开发过程中咱们也常须要对某个办法进行 mock(即对原有办法设置一个长期代替办法并且调用形式保持一致),比方某个函数拜访 Redis、DB 这种状况下进行单元测试往往须要对这些办法进行 mock,不便其余函数调用进行单元测试,为了不便进行单元测试咱们往往会对其进行 mock,所以为了不便开发人员进行疾速的 mock,所以咱们提供了 tu::mock 的注解帮忙开发同学疾速的定义注解,而后 TU 会主动生成对应的 mock 函数。例如:当初给 foo_read 办法 mock 一个函数,让 mock 的函数返回 10:
三、TU 实现计划
3.1 AST 是什么?
GENERIC、GIMPLE 和 RTL 三者形成了 gcc 两头语言的全副,它们以 GIMPLE 为外围,由 GENERIC 承上,由 RTL 启下,在源文件和指标指令之间的鸿沟之上构建了一个三层的过渡。
GCC 在语法分析过程中,所有辨认进去的语言部件都用一个叫 TREE 的变量保留着。这个 TREE 就是 GCC 语法树(AST),这个过程叫做 GENERIC。实际上它也是 GCC 的符号表,因为变量名、类型等等这些信息都由 TREE 关联起来。
上面咱们通过 gcc 编译选项来看下 gcc 的 ast 表现形式:
3.2 AST(Abstract syntax tree)
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 反对的所有插件类型如下图所示:(摘自 gcc 6.3.0 源码)
四、TU 插件应用的繁难水平比照
如果仅仅只是做边界测试那么仅须要批改构建的脚本比方 cmake 增加对应的插件参数即可。
五、应用 TU 的长处
- 接入简略、边界单元测试能够做到业务代码 0 批改
- 函数参数可边界值实现全排列,大大降低用例脱漏危险、缩小大量重复性的工作
- 疾速生成用户自定义用例、mock 办法等
六、TU 反对的性能
七、总结与瞻望
1、文章中比照了三种办法主动生成测试用例的办法,上面对这几种办法进行比照:
2、文章中还次要介绍了 TU 的性能特点以及基于 GCC-AST 的实现主动生成测试用例的解决方案。
TU 解决方案目前在构建时可能主动生成测试用例曾经极大升高了单元测试门槛晋升单元测试覆盖率,将来咱们也心愿可能把 TU 与 IDE 相结合,摸索更高效便捷的应用形式,通过更加便捷的形式生成指定办法的测试用例。比方通过在函数、办法上,通过快捷键生成以后办法的测试用例等。
参考文献:
【1】gcc plugins
【2】Functions for C++ (GNU Compiler Collection (GCC) Internals)