一、背景介绍

随着项目迭代的不断深入,工程逻辑与用户场景日益复杂,传统的白盒测试体系已经无法适应苛刻的工程质量要求,因此有必要针对工程质量进行精细化管理。 质量评估不再单纯依赖bug率和性能指标,而是通过精准的数据来量化代码质量,代码覆盖率就是其中的一项重要标准。

简单来说,代码覆盖率就是单元测试或者UI测试过程中对于被测代码的覆盖程度,可分为以下三种度量方式:

  • StatementCoverage:最基础的一种覆盖方式,用以度量被测代码中每个可执行语句是否都被执行。可执行语句不包括头文件声明、宏定义、代码注释等
  •  Branch Coverage:分支覆盖,度量程序中每一个判定的分支是否都被测试
  •  Loop Coverage:度量对循环体进行了多少次覆盖

 

在实践中,通常采用Statement Coverage方式来分析覆盖率数据。拿到覆盖率数据之后,分析未覆盖部分的代码,可以反推出测试是否充分,前期的测试用例设计是否合理,没有覆盖到的代码是否是设计的盲点。同时其对于冗余代码的检测具有重要的参考意义, 可以逆向反推出代码设计中存在的问题,提醒开发人员理清代码逻辑关系,提升代码质量。

代码覆盖率高不能说明代码质量高,但是代码覆盖率低,意味着代码质量很可能存在一些问题,因此代码覆盖率可以作为代码质量的一个重要衡量标准。

 



二、原理与演示



获取代码覆盖率的前提是插入计数器,也就是插桩,从直观上讲,可以分为三种方案:

  • 源代码插桩:直接在源码中的每行可执行语句中加入计数器。该方案开发成本太高,且影响包大小和运行时性能
  • 中间文件插桩:在编译过程的中间文件插入汇编代码,成本低,速度快,是理想的插桩方案
  • 可执行文件插桩:目前暂时没有合理的方案。

因此,我们采用中间文件插桩方案,过程如下图所示:

Emma代码覆盖率实战 代码覆盖率原理_测试用例

源代码文件首先经过编译预处理,之后编译成汇编文件,在生成汇编文件的同时完成插桩。每个桩点插入若干条汇编语句,直接插入生成的*.s文件中,最后将汇编文件生成目标文件。在程序运行过程中桩点负责收集程序的执行信息。

以源文件test.c为例:

Emma代码覆盖率实战 代码覆盖率原理_数据_02

预处理命令(gcc -E test.c -otest.i)之后,生成test.i文件,对该文件进行编译(gcc-fprofile-arcs -ftest-coverage -S test.i),生成test.s汇编文件。可以看到gcc是通过增加编译选项-fprofile-arcs -ftest-coverage来进行插桩。

打开汇编文件test.s,其中可以看到代码运行过程中计数器增加的逻辑:

Emma代码覆盖率实战 代码覆盖率原理_数据_03

上图中展示了计数器自增的过程,将llvm_gcov_ctr赋值给寄存器rax,之后将rax加1,最后将rax赋值给llvm_gcov_ctr。

 

除了上述逻辑,汇编文件中还列出了覆盖率收集的入口方法__gcov_init, 该方法中的逻辑为:

Emma代码覆盖率实战 代码覆盖率原理_Emma代码覆盖率实战_04

在可执行文件进入用户代码段main函数之前,先调用__gcov_init函数初始化统计数据区,即将所有文件的gcov_info结构组织成一个链表,链表头就是gcov_list,其为全局链表,将在输出统计数据时应用。

之后__gcov_init调用atexit (gcov_exit)将gcov_exit函数注册为exit handlers。

App运行完调用exit正常结束时,gcov_exit函数就会得到调用,其内部调用gcov_flush函数输出统计数据到 *.gcda 文件中,也就是从gcov_list的第一个gcov_info结构开始,为每个被测文件生成一个.gcda文件。.gcda文件的主要内容就是gcov_info结构的内容。

插桩之后,会生成test.gcno文件。运行可执行文件后,生成test.gcda文件。

最后,运行gcov test.c命令可生成test.c.gcov文件,如下所示:

Emma代码覆盖率实战 代码覆盖率原理_代码覆盖率_05

每行前面的数字表示被执行次数,####表示该行未被执行,需要重点关注。

 

由上文可以看出,覆盖率数据分析需要三类文件:

  • 源代码文件
  • gcno:包含基本的块信息,以及代码行与块的映射关系,也就是保存计数插桩位置和源文件之间关系
  • gcda:包含代码行执行的情况,以及覆盖率的信息归纳

其中gcno和gcda文件可以看做是覆盖率分析的中间文件。如果出于保密要求,无法提供源文件,也可以分析出总体的行覆盖率和方法覆盖率,只不过无法将覆盖率与代码逻辑进行一一对应。




三、iOS平台的接入



下文将以iOS平台为例,探讨代码覆盖率在工程中的具体实践。

目前,XCode已经提供了运行时覆盖率的展示,但是现有覆盖率存在以下问题:

  • 只支持调试模式下的单元测试覆盖率
  • 无法离线获取覆盖信息
  • 无法合并多个设备、多个版本的覆盖信息

接下来介绍如何解决这些问题。




1.工程插桩



Emma代码覆盖率实战 代码覆盖率原理_测试用例_06

在待统计Target的Build Settings中分别设置Instrument Program Flow、Generate Legacy Test Coverage File为True,即可快速打开插桩。需要注意的是,为了控制包大小和运行性能,一般只在Debug环境下进行插桩。

 




2.模拟器与真机的区别



插桩打开并完成编译后,在编译缓存目录下会生成与源文件对应的gcno文件。

App运行后,通过在某一时机(如切入后台时,或者定时处理)调用__gcov_flush()方法将覆盖信息写入gcda文件。模拟器下,gcda与gcno文件处于同一目录。真机中,gcda文件会在沙盒生成,需要设置沙盒中的目录名称和path深度,如下所示:

Emma代码覆盖率实战 代码覆盖率原理_测试用例_07

 




3.系统架构



针对代码覆盖率的多种使用场景,我们设计出一站式解决方案,架构图如下所示:

Emma代码覆盖率实战 代码覆盖率原理_代码覆盖率_08

其中底层依赖于OS及其对应的插桩工具,如gcc。上层文件系统包括编译插桩配置,预植入的git hook定制脚本,App分发机制。该层为覆盖率中间文件的生成提供技术支持。

数据采集主要是将分散在打库机和各设备中的覆盖率中间文件收集起来。打库机提供了编译时生成的gcno文件并上传,带有覆盖计数器的App运行时产生了gcda文件并上传,数据归集模块则将二者按照版本号、设备ID等标识一一对应、等待分析。

全量分析是针对某时刻全部源码所对应的覆盖率数据,通常需要经过过滤、容错、分析、合并四个环节,该过程部分依赖于lcov工具。lcov最终将gcno与gcda文件解析为中间info文件,该文件与对应的源代码文件结合在一起,可以定制生成详细的报告文档。

增量分析是针对某段时间或版本所产生的增量代码所对应的覆盖率数据。为了降低开发成本,增量覆盖率是由全量覆盖率经过源码差异映射、增量过滤、增量分析而产生。

由于已分析输出的全量和增量覆盖率皆为文本文件,可读性不佳,无法快速查询代码细节。因此,有必要增加报告层。现有的报告层可以根据默认模板或自定义模板输出对应的XML和H5类型的完整报告。

 



四、使用场景




1.版本迭代全量与增量覆盖率



其中某个模块所对应的全量覆盖率数据展示如下:

Emma代码覆盖率实战 代码覆盖率原理_测试用例_09

有了全量覆盖率,可以通过增量分析模块获得增量覆盖率数据,默认展示样式可以参考上图。

通过查询详细的覆盖率报告,代码和模块覆盖率偏低的情况的产生一般有如下原因:

  • 受后台数据等影响,在当前版本中未打开代码逻辑开关
  • 代码本身逻辑:如父类的方法被子类覆盖,导致父类代码无法被运行
  • 冗余代码:可以通过报告快速确认冗余代码范围,以及不属于本模块的代码
  • 部分弹框、浮层等需要全新安装(而不是覆盖安装)或者一定时机触发的场景,需要评估是否有必要专门建立满足场景触发的case

积累了数个版本的全量覆盖数据,就可以进行预警机制建设。输出每日开发自测、测试人员手动测试、自动测试覆盖率,分析合理的增长趋势。同时整体考量以下因子:前置产品、资源与接口、开发进度、测试进度、bug产生与修复进度、发版后的bug追溯机制。如果偏离该趋势、进度延误或者线上bug复现,则及时进行预警。




2.代码审核增量覆盖率



提交的增量覆盖率示例如下:

Emma代码覆盖率实战 代码覆盖率原理_测试用例_10

其中列出了本次提交的代码变更行数、被覆盖的行数以及增量覆盖率。中间位置详细列出了未被覆盖的行号,点击之后会立即定位到相应的代码行。

开发者提交代码前,先在模拟器或真机上自测。commit时,预先植入git hook中的脚本会自动进行提交代码的增量覆盖率分析,生成报告文档并上传服务端,得到一条覆盖率远程url。commit后,commit message末尾会自动附加该url。

本次提交push之后,代码审核人员可以参考该覆盖率数据。该过程对开发者透明,没有接入成本。审核人员根据该数据可以快速定位开发自测的充分性和有效性。同时支持根据需要设定提交代码覆盖率的合法阈值,未达到该阈值可以直接拒绝提交或增加提示。




3.单元测试覆盖率



单元测试的质量评估主要依赖于测试用例的成功率和代码覆盖率。因此我们推动了各模块发版前覆盖率检查、自动验证流程的建设。推动垂线模块合入代码前的有效性验证,包括bug修复率、线上运行的覆盖率统计报告,最终目标是单元测试0错误率,覆盖率超过60%时才允许合入代码库。




4.自动化测试覆盖率



通过分析自动化测试执行过程中的覆盖率数据,可以建立起测试用例与程序代码之间的逻辑关系。

对于开发人员来说,可以看到测试人员执行用例的代码细节有助于进行缺陷的修复,测试数据可以直接为开发调试提供依据、快速定位并修复缺陷。

对于测试人员来说,通过分析每条测试用例对覆盖率的贡献可以精简用例集、剔除无效用例。测试人员通过修改的源代码快速确定测试用例的范围,减少回归测试的盲目性和工作量,快速修订测试用例,达到测试覆盖率最大化,从而提高测试效率。



五、总结



通过引入代码覆盖率分析体系,可以精确把控增量代码质量,持续改善优化存量代码。 同时将测试用例的影响范围细化到代码层面,从而实现精准化测试。 目前这套体系作为一项开发流程规范进行实践,如何在实践过程中减少人工参与的成本、进行用例的智能推荐或生产,将是我们下一步工作的重点。







end