作者:vivo 互联网服务器团队- Xu Shen

本文主要介绍vivo内部研发平台使用JaCoCo实现测试覆盖率的实践,包括JaCoCo原理介绍以及在实践过程中遇到的新增代码覆盖率统计问题和频繁发布导致覆盖率丢失问题的解决办法。

一、为什么需要测试覆盖率

1.1 在日常研发过程中,经常发现一些问题

  • 测试案例的设计凭经验,当研发一个新功能时,经常对测试场景估计不足,到上线后发现bug;

  • 开发经常做一些需求之外的代码变更(代码小范围内重构或在开发过程中发现小缺陷随手改掉),导致测试任务无法测试到对应的场景,引起线上问题;

  • 对测试效果无法量化考核,导致测试工作的质量无法进一步提升。

1.2. 有没有技术手段能够尽可能的避免上面的问题呢?

在业内已经在普遍使用代码覆盖率来提升测试质量,那什么是代码覆盖率?

代码覆盖率是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率 

代码覆盖率指标通常包含下面几类:

  • 函数/方法覆盖率:函数/方法中有多少被调用到

  • 分支覆盖率:有多少控制结构的分支(例如if语句)被执行

  • 条件覆盖率:有多少布尔子表达式被测试为真值和假值

  • 行覆盖率:有多少行的源代码被测试过

1.3 在使用测试覆盖率的过程中,经常发现的场景

  • if/else语句中,if{}内的代码被覆盖到,else{}内的代码没有被覆盖到,可以得出部分分支场景没有测试到;

  • try/catch语句中,try{}内的代码被覆盖到,catch{}内的代码没有被覆盖到,可以得出异常场景没有测试到;

  • if (条件1 || 条件2 || 条件3)语句中,条件1被覆盖到,条件2和条件3没有被覆盖到,可以得出部分条件场景没有测试到;

测试人员对代码覆盖率的指标正确使用,能有效提升测试的质量,进而提升版本的上线质量。

二、JaCoCo在测试覆盖率场景中的使用

2.1  JaCoCo介绍

当前主流的代码覆盖率工具:

C/C++→Gcov ,Java→JaCoCo,JavaScript→ Istanbul。

考虑到服务器端主要是Java语言,所以CICD平台优先使用JaCoCo来支持 Java 语言的代码覆盖率统计能力。

通过JaCoCo官网,我们可以看到JaCoCo的使命是为Java VM 的环境中的代码覆盖分析提供标准技术。重点是提供一个轻量级、灵活且有据可查的库,用于与各种构建和开发工具集成。

2.2 JaCoCo优点

  • JaCoCo支持指令(C0)、分支(C1)、行、方法、类和圈复杂度等多维度的覆盖分析;

  • 基于 Java 字节码,也可以在没有源文件的情况下工作;

  • 性能良好,运行时开销很小,尤其是对于大型项目;

  • 比较完整的API,很方便与其他工具进行集成;

  • 远程协议和 JMX 控制可在任何时间点从代理请求执行数据下载。

2.3 JaCoCo原理

主要来自于JaCoCo官方网站

JaCoCo支持几种不同的方法来收集覆盖信息,对于每种方法,由不同技术实现的,下图橙色路径部分是JaCoCo 推荐使用的方式,即通过On-The-Fly方式收集覆盖率信息:

通过上图我们知道,JaCoCo 是通过对Java字节码(Byte Code)插入探针的方式来收集覆盖率信息的,探针是可以插入现有指令之间的附加指令。它们不会改变方法的行为,但会记录它们已被执行的事实。

下面以一段简单的  程序为例进行说明:

这段代码经过Java编译以后转化为以下字节码:

因为Java 字节码指令的线性序列,控制流是通过条件或无条件指令实现跳转的,跳转目标在技术上是相对于目标指令的偏移量。这个跟大学学习的汇编指令的跳转方式类似,为了更好的可读性,使用符号标签 (L1,L2 ) 代替实际的指令地址。

上图中橙色的部分为插入的探针,理论上我们可以在控制流图的每个边缘插入一个探针,由于探针实现本身需要一些字节码指令,这将会使类文件的大小增加数倍;幸运的是,这不是必需的,实际上我们只需要根据方法的控制流为每个方法插入几个探针。例如,没有任何分支的方法只需要一个探针。

如果已经执行了探测,我们就知道相应的边已经被访问过。从这条边我们可以得出结论到其他前面的节点和边:

  • 如果一条边被访问过,我们就知道这条边的源节点已经被执行了;

  • 如果一个节点已经被执行并且该节点是只有一条边的目标,我们知道这条边已经被访问过。

如果我们在正确的位置有探针,递归地应用这些规则可以确定方法的所有指令的执行状态,探针只是需要在控制流边缘插入的一小段附加指令。

三、CICD平台关于测试覆盖率的解决方案

通过上面对JaCoCo原理的介绍,结合我们公司内部的研发流程,在CICD平台对代码覆盖率功能的设计如下:

从上面 CICD 平台对测试覆盖率的设计图,大概可以看出来,整个过程包含三个阶段

3.1 测试前

测试前由测试人员(开发人员/运维人员)在流水线上开启测试覆盖率功能,在流水线执行发布时,会在测试环境上下载JaCoCo Agent包,并在Java进程启动时配置JavaAgent参数;

在进程启动过程或启动之后,有class文件被加载时被Agent拦截,对class文件进行插桩处理,在必要的路径下插入探针(插入探针的原理在上一节已经介绍)。

3.2 测试中

在测试过程中,测试人员在测试环境执行测试案例(手动执行或自动化脚本),被调用到的代码会被探针记录下来,探针数据保存在Java进程的内存中。

3.3  测试后

测试人员可以多次发布测试环境,针对同一个分支的代码,可以合并多次测试的结果数据,形成全量的覆盖率数据;

在测试结束后,CICD平台通过JaCoCo的API,手动/自动下载(dump)覆盖率数据,合并(merge)历史覆盖率数据,生成测试覆盖率报告;

测试人员根据测试覆盖率报告的结果,查看测试遗漏的场景,进行补充测试,事后总结遗漏的原因,提高测试效率。

四、在实践过程中遇到的问题及解决办法

测试覆盖率在上线运行一段时间后,在实践过程中发现了一些问题,总结为以下几点:

4.1 在不同机器编译会导致classid不一致的问题

在实践过程中,经常遇到这样一个问题,用户反馈并确认案例已经正常执行,但是生成的报告显示未覆盖,经过调查发现在测试环境中的class和生成报告时的class不一致导致的。

在 JaCoCo内部,覆盖率数据是以classid作为key来存储的,classid是根据class的字节码hash算法得出来的,看JaCoCo源码中关于classid的算法如下:

出现不一致的情况包括:

  • 发布时编译的机器和生成报告的机器环境上有差异,比如操作系统版本、JDK版本等,导致编译的class不一致;

  • 发布时编译的代码版本与生成报告时的代码版本有差异,导致编译的class不一致。

要解决上面环境的问题,需要保持在测试覆盖率过程中编译的机器环境保持一致,或者做到只编译一次,使用同一份class文件,考虑到存储空间的问题,vivo采用保持环境一致的办法来解决。

对于第二种情况,常见于采用敏捷研发的团队,在一个版本中按功能点转测,经常导致测试在测试过程中,源代码已经发生了修改,生成报告时代码版本和发布时的代码版本已经不一致,这种情况比较复杂,我们在下面会介绍。

4.2 在研发过程中更加关注增量代码的覆盖率

在我们日常的研发活动中,对于全量代码更多使用自动化脚本来回归,而新研发的功能主要表现为增量代码,对于增量代码的覆盖率情况更加关注, JaCoCo本身不支持增量代码的覆盖率。

对于这个问题网上也有不少解决方案,基本都是基于git的版本差异,在生成报告时过滤掉没有差异的类,形成两份覆盖率报告,一份是全量代码覆盖率报告,一份是增量代码覆盖率报告,而我们更希望在一份覆盖率报告中呈现增量代码和全量代码的覆盖情况,结合代码在全量报告中的覆盖路径分析遗漏的场景,同时能在报告中标注增量代码和增量代码的覆盖情况,期望的效果如下图所示:

为了达到上述效果,需要几个改造步骤:

  • 计算出当前代码分支的变动情况,需要精确到代码行

  • 改造JaCoCo计算逻辑,针对增量代码单独统计覆盖率指标值

  • 改造JaCoCo报告格式,在报告中兼容全量代码和增量代码的覆盖情况

对于计算代码分支的变动情况,放弃 GitLab 提供的代码比对功能来获取不同版本之前的差异信息,如果版本之间差异太多的话,经常发生GitLab 的API接口调用超时;

并且GitLab 的比对功能无法满足定制场景,比如一行代码仅仅因为格式化被识别为变更代码等等,采用借助Linux自带的diff命令,实现代码差异比对的能力:

对于改造 JaCoCo计算逻辑,增加针对增量代码的覆盖率指标统计,在CoverageNodeImpl类中增加新的Counter,用于统计新增类、方法、行、指令覆盖率指标;在SourceNodeImple类中increment方法中增加新增代码行的统计逻辑。

4.3 重谈关于classid的问题

在上面已经谈到关于classid的问题,如果是环境问题是比较好解决,但是现在互联网团队基本都使用敏捷模式,基本不太可能等开发工作全部完成再转测,这样必然会导致最新的覆盖率报告,会出现以类为单元的覆盖率数据丢失,需要测试人员来回重复的执行测试案例,否则测试覆盖率数据不会很好看。

既然知道问题所在,那有没有办法解决呢?是不是可以直接找到以前的classid,把以前的classid对应的探针数据复制到当前的classid下就可以?当然是不行的,因为源代码发生变动,导致探针的数量发生变化,会出现下面的情况:

或者这样

出现这样的情况,会无法判断具体哪些探针是新增的或者删除的;即使出现前后探针一致的情况,也有可能因为代码修改,探针位置发生变化:

那么这个问题是否就无解了呢?这里给出一个大概思路,现在的覆盖率数据是以类为单位存储的,我们可以修改存储的粒度,细化到方法级别,这样可以保留一个类的大部分探针数据,这样如果只是修改一个方法的话,那么其他方法的测试数据可以继续保留,只需要重新测试这个方法就行,这样可以有效的降低测试人员对整个类的所有方案重复测试的情况。

五、总结

对于测试覆盖率功能,有没有给测试的质量带来提升,答案是显而易见的。

当然也因为上面提到的问题,给测试人员带了些麻烦,为了提升测试覆盖率数据,导致测试人员对同一个功能重复多次测试;同时也给测试人员带来了好处,很多测试人员在面对测试覆盖率指标严格要求下,被迫去看代码的实现逻辑,提升了自己业务水平和阅读代码的水平,甚至出现测试人员和开发人员当面对质,关于代码逻辑是否合理的场景。

最后,测试覆盖率不是衡量测试质量的唯一标准,要合理利用测试覆盖率来提升测试质量。

vivo 基于 JaCoCo 的测试覆盖率设计与实践的更多相关文章

  1. atitit.基于http json api 接口设计 最佳实践 总结o7

    atitit.基于http  json  api 接口设计 最佳实践 总结o7 1. 需求:::服务器and android 端接口通讯 2 2. 接口开发的要点 2 2.1. 普通参数 meth,p ...

  2. 基于spring-boot的测试桩设计-添加配置文件(properties)

    编写测试时,有些内容可以放到配置文件中. 第一步:新增配置文件 conf.properties 第二步:编写配置文件类 MockConf package mock.mockdemo.conf; imp ...

  3. 基于spring-boot的测试桩设计--几种常见的controller

    第一种:通过@RequestBody,直接将请求体映射到对象 //@RequestBody @RequestMapping(value = "addUser", method = ...

  4. vivo 敏感词匹配系统的设计与实践

    一.前言 谛听系统是vivo的内容审核平台,保障了vivo各互联网产品持续健康的发展.谛听支持审核多种内容类型,但日常主要审核的内容是文本,下图是一个完整的文本审核流程,包括名单匹配.敏感词匹配.AI ...

  5. vivo 基于原生 RabbitMQ 的高可用架构实践

    一.背景说明 vivo 在 2016 年引入 RabbitMQ,基于开源 RabbitMQ 进行扩展,向业务提供消息中间件服务. 2016~2018年,所有业务均使用一个集群,随着业务规模的增长,集群 ...

  6. 什么是基于风险的测试(RBT)?

    基于风险的测试(Risk-based testing) 文/杨学明 一.基于风险的测试起源 基于风险的测试起源,在软件测试领域,基于风险测试最早的是测试大师Boris Beizer<软件测试技术 ...

  7. iOS 覆盖率检测原理与增量代码测试覆盖率工具实现

    背景 对苹果开发者而言,由于平台审核周期较长,客户端代码导致的线上问题影响时间往往比较久.如果在开发.测试阶段能够提前暴露问题,就有助于避免线上事故的发生.代码覆盖率检测正是帮助开发.测试同学提前发现 ...

  8. 基于Verilog HDL整数乘法器设计与仿真验证

    基于Verilog HDL整数乘法器设计与仿真验证 1.预备知识 整数分为短整数,中整数,长整数,本文只涉及到短整数.短整数:占用一个字节空间,8位,其中最高位为符号位(最高位为1表示为负数,最高位为 ...

  9. [python 译] 基于面向对象的分析和设计

    [python 译] 基于面向对象的分析和设计 // */ // ]]>   [python 译] 基于面向对象的分析和设计 Table of Contents 1 原文地址 2 引言 2.1 ...

随机推荐

  1. Hyper-v安装虚拟机,提示the image's hash and certificate are not allowed错误的解决方法

    本文迁移自Panda666原博客,原发布时间:2021年3月29日. Hyper-v安装虚拟机,提示the image's hash and certificate are not allowed错误 ...

  2. Linux 文件权限相关知识

    文件权限说明 Linux中的文件能否被访问和工具(程序)无关,和访问的用户身份有关(谁去运行这个程序) 进程的发起者(谁去运行这个程序). 进程的发起者若是文件的所有者: 拥有文件的属主权限 进程的发 ...

  3. 所有人都说Python 简单易学,为何我觉得难?

    来谈谈心 记得刚学Python的时候,几乎所有人都说Python 简单易学,而对于编程零基础,只掌握Word和Excel的人来说,感觉真的好难. 学习之前网上的教材看了,Python的书也看了,包括& ...

  4. 让你的Nginx支持分布式追踪

    Background NGINX 是一个通用且流行的应用程序.也是最流行的 Web 服务器,它可用于提供静态文件内容,但也通常与其他服务一起用作分布式系统中的组件,在其中它用作反向代理.负载均衡 或 ...

  5. JAVA中简单的for循环竟有这么多坑,你踩过吗

    JAVA中简单的for循环竟有这么多坑,你踩过吗 实际的业务项目开发中,大家应该对从给定的list中剔除不满足条件的元素这个操作不陌生吧? 很多同学可以立刻想出很多种实现的方式,但你想到的这些实现方式 ...

  6. 线程池的概念&原理和线程池的代码实现

    线程池:一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源.工作原理:可以用一张图来简洁明了说明: 合理利用线程池能够带来三个好处∶1.降低 ...

  7. Python基于周立功ZCANPRO开发刷写脚本

    一.概述 1.背景 本文章主要是记录用Python基于周立功ZCANPRO开发VIN和SN码刷写工具. 2.环境搭建 Python3.8.10 32位(必须) 周立功上位机:ZCANPRO 周立功CA ...

  8. 面试突击64:了解 HTTP 协议吗?

    HTTP(Hyper Text Transfer Protocol)超文本传输协议,下文简称 HTTP,它的作用是用于实现服务器端和客户端的数据传输的.它可以传输任意的数据类型,如文本.HTML.图片 ...

  9. Qt+ECharts开发笔记(三):ECharts的柱状图介绍、基础使用和Qt封装Demo

    前言   上一篇成功是EChart随着Qt窗口变化而变化,本篇将开始正式介绍柱状图介绍.基础使用,并将其封装一层Qt.  本篇的demo实现了隐藏js代码的方式,实现了一个条形图的基本交互方式,即Qt ...

  10. [Java学习笔记] Java异常机制(也许是全网最独特视角)

    Java 异常机制(也许是全网最独特视角) 一.Java中的"异常"指什么 什么是异常 一句话简单理解:异常是程序运行中的一些异常或者错误. (纯字面意思) Error类 和 Ex ...