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的优点

  1. 接入简单、边界单元测试可以做到业务代码0修改

  2. 函数参数可边界值实现全排列,大大降低用例遗漏风险、减少大量重复性的工作

  3. 快速生成用户自定义用例、mock方法等

六、TU支持的功能

七、总结与展望

1、文章中对比了三种方法自动生成测试用例的方法,下面对这几种方法进行对比:

2、文章中还主要介绍了TU的功能特点以及基于GCC-AST的实现自动生成测试用例的解决方案。

TU解决方案目前在构建时能够自动生成测试用例已经极大降低了单元测试门槛提升单元测试覆盖率,未来我们也希望能够把TU与IDE相结合,探索更高效便捷的使用方式,通过更加便捷的方式生成指定方法的测试用例。比如通过在函数、方法上,通过快捷键生成当前方法的测试用例等。

参考文献:

【1】gcc plugins

【2】Functions for C++ (GNU Compiler Collection (GCC) Internals)

C/C++ 单元自动化测试解决方案实践的更多相关文章

  1. <自动化测试方案_5>第五章、代码单元自动化测试

    第五章.代码单元自动化测试 代码单元测试需要根据编程语言,选择单元测试框架,然后访问类方法,函数.代码单元测试做自动化,相比API.UI自动化做起来更加麻烦,建议放到待API自动化测试.UI自动化测试 ...

  2. 自动化测试调查问卷送《QTP自动化测试最佳实践》

    自动化测试调查问卷送<QTP自动化测试最佳实践> http://automationqa.com/forum.php?mod=viewthread&tid=2308&fro ...

  3. Atitit  文件上传  架构设计 实现机制 解决方案  实践java php c#.net js javascript  c++ python

    Atitit  文件上传  架构设计 实现机制 解决方案  实践java php c#.net js javascript  c++ python 1. 上传的几点要求2 1.1. 本地预览2 1.2 ...

  4. atitit.jndi的架构与原理以及资源配置and单元測试实践

    atitit.jndi的架构与原理以及资源配置and单元測试实践 1. jndi架构 1 2. jndi实现原理 3 3. jndi资源配置 3 3.1. resin  <database> ...

  5. Web前端自动化测试Cypress实践总结

    本文主要首先主要介绍了什么是自动化测试,接着对常用的自动化测试框架进行了对比分析,最后,介绍了如果将自动化测试框架Cypress运用在项目中. 一.自动化测试概述 为了保障软件质量,并减少重复性的测试 ...

  6. 微信小程序自动化测试最佳实践(附 Python 源码)

    本文为霍格沃兹测试学院测试大咖公开课<微信小程序自动化测试>图文整理精华版. 随着微信小程序的功能和生态日益完善,很多公司的产品业务形态逐渐从 App 延升到微信小程序.微信公众号等.小程 ...

  7. KVM + LinuxBridge 的网络虚拟化解决方案实践

    目录 文章目录 目录 前言 Linux bridge 的基本操作 创建 Bridge 将 veth pair 连上 Bridge 为 Bridge 配置 IP 地址 将物理网卡接口设备挂靠 Bridg ...

  8. 自动化测试ROI实践

    自动化测试是一项"一旦开始,就需要持续投入"的工作,所以它一直是测试领域的一块鸡肋.不做吧,好像手工测试重复得让人有些厌倦,而且手工测试时间也缩短不了.做吧,害怕投入的比回报要多. ...

  9. 手动从0搭建ABP框架-ABP官方完整解决方案和手动搭建简化解决方案实践

      本文主要讲解了如何把ABP官方的在线生成解决方案运行起来,并说明了解决方案中项目间的依赖关系.然后手动实践了如何从0搭建了一个简化的解决方案.ABP官方的在线生成解决方案源码下载参考[3],手动搭 ...

随机推荐

  1. Mybatis模糊查询结果为空的解决方案

    写在前面 Mybatis使用模糊查询,查询结果为空的解决方案,我的代码是 select * from sp_user where 1=1 <if test="username!=nul ...

  2. docker 国内镜像源

    参考:docker 国内镜像源 作者:Joncc Linux环境 # vi /etc/docker/daemon.json { "registry-mirrors": [" ...

  3. python---单链表的常用操作

    class Node(object): """结点""" def __init__(self, data): self.data = dat ...

  4. Codeforces Round #677 (Div. 3) D. Districts Connection思维1200

    题目链接 Problem - 1433D - Codeforces 题目 美好的一天,从看不懂题目开始~~ Example input 4 5 1 2 2 1 3 3 1 1 1 4 1 1000 1 ...

  5. javascript中的Ajax基础(一)

    一.手写一个ajax 1 const xhr = new xmlHttpRequest() 2 3 xhr.open(请求方式:post get, 请求地址, 同步或者异步) 4 5 xhr.onre ...

  6. MySQL 数据库备份脚本

    MySQL 数据库备份脚本 #!/bin/bash # 数据库连接信息 DB_HOST="127.0.0.1" DB_PORT="3306" DB_USER=& ...

  7. 基于Ansible实现Apache Doris快速部署运维指南

    Doris Ansible 使用指南 Apache Doris 介绍 Apache Doris是一个现代化的MPP分析型数据库产品.仅需亚秒级响应时间即可获得查询结果,有效地支持实时数据分析.Apac ...

  8. IDEA编译项目后,target目录下的jsp文件不更新

    tomcat目录说明 先来看一下tomcat的目录: |-bin |-conf |-lib |-logs |-temp |-webapps |-work tomcat 的核心是servlet容器,叫 ...

  9. 代码管理工具-Git基础介绍及常用技巧

    目录 Git起源 基本概念 Branch.HEAD和Commit tree Git分支 git merge 和 git rebase 的区别和抉择 与远程仓库的交互 关于一些实际开发场景的问题和解决方 ...

  10. MySQL性能优化 - 别再只会说加索引了

    MySQL性能优化 MySQL性能优化我们可以从以下四个维度考虑:硬件升级.系统配置.表结构设计.SQL语句和索引. 从成本上来说:硬件升级>系统配置>表结构设计>SQL语句及索引, ...