写点什么

【得物技术】代码覆盖率原理与得物 app 实践

用户头像
得物技术
关注
发布于: 2021 年 01 月 15 日
【得物技术】代码覆盖率原理与得物app实践

一、前言

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


二、代码覆盖率简述

2.1 什么是代码覆盖率

代码覆盖率测试技术是一种常见的白盒测试技术,是衡量软件测试工作充分性和完整性的重要指标之一。


简单来说,代码覆盖率就是测试过程中已经被执行过的代码占准备测试总代码量的比例和程度,它关注的是在执行用例时,有哪些代码被执行到了,有哪些代码没有被执行到。 

2.2 代码覆盖率的价值

代码覆盖率的分析在一定程度上能够评判代码质量,一般覆盖率高的代码出错的几率会相对低一些。但是高覆盖率的代码只能表示执行了很多的代码,并意味着这些代码被很好的执行了。


首先,对于测试来说,代码覆盖率最主要的意义是帮助我们了解测试情况,可以通过分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,之前为什么没有考虑到?或许是需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。


其次,其有助于发现多个测试用例都覆盖不到的代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑,提升代码质量;同时为废弃代码提供依据。


此外,代码覆盖率可以度量单元/自动化测试用例,提供覆盖率统计情况,可以通过分析覆盖率报告,完善用例。


最后,代码覆盖率利于精准回归,通过构建代码调用关系,精准的确定回归测试范围,避免了全量回归造成的测试资源浪费。

2.3 常用指标

  • 语句覆盖

又称行覆盖(LineCoverage),指已经被执行到的语句占总可执行语句(不包含类似 C++的头文件声明、代码注释、空行等等)的百分比,这是最常用的也是要求最低的覆盖率指标,实际中通常会结合判定覆盖率或者条件覆盖率一起使用。

  • 判定覆盖

  • 又称分支覆盖(BranchCoverage),用以度量程序中每一个判定的分支是否都被测试到了,即代码中每个判定的"真"和"假"至少执行一次。

  • 这句话是需要进一步理解的,应该非常容易和下面说到的条件覆盖混淆。因此我们直接介绍第三种覆盖方式,做个对比,就明白两者是怎么回事了。

    • 条件覆盖

  • 度量判定中的每个子表达式结果 true 和 false 是否都被测试到了

    • 路径覆盖率、函数覆盖率、类覆盖率、指令覆盖率等指标

    为了说明判定覆盖和条件覆盖的区别,我们来举一个例子,假如我们的被测代码如下:

    int foo(int a, int b){  if (a < 10 || b < 10) // 判定  {    return 0; // 分支一  }  else  {    return 1; // 分支二  }}
    复制代码

    在设计判定覆盖的测试用例时,我们只需要考虑到判定结果为 true 和 false 两种情况,因此我们只需要设计如下的 case,就能达到判定覆盖率 100%:

    a = 5, b = 任意数字  覆盖了分支一

    a = 15, b = 15   覆盖了分支二


    设计条件覆盖案例时,我们需要考虑到判定中每个表达式的结果,为了达到覆盖率 100%,设计了如下案例:

    a=5    (条件 a<10 的值为“真”)

    a=15   (条件 a<10 的值为“假”)

    b=5    (条件 b<10 的值为“真”)

    b=15   (条件 b<10 的值为“假”)


    通过上面的例子,应该很清楚的了解了判定覆盖和条件覆盖的区别。

    需要注意的是:条件覆盖不是将判定中的个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果 true 和 false 测试到就可以了。

    同时,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖。  

    2.4 JAVA 覆盖率工具介绍

    目前 Java 常用的覆盖率工具有 JaCoCo、Emma 和 Cobertura、Clover(商用),详细介绍请看下表:


    其中 Emma 和 Cobertura 已经停止维护了,只有 JaCoCo 还在不断的更新,JaCoCo 社区也比较活跃,所以现在使用的最为广泛的就是 JaCoCo 了。

    目前我司选用的也是 JaCoCo。下面让我们一起来看下 JaCoCo 的原理、使用和在公司的实践吧。

    三、关于 JaCoCo

    3.1 JaCoCo 简述

    JaCoCo 是一个开源的覆盖率工具

    (官方文档地址:

    https://www.jacoco.org/jacoco/trunk/doc/index.html)

    它针对的开发语言是 java,其使用方法很灵活,可以嵌入到 Ant、Maven 中;可以作为 Eclipse 插件;也可以使用 JavaAgent 技术监控 Java 程序等等。

    很多第三方的工具提供了对 JaCoCo 的集成,如 sonar、Jenkins 等。

    3.2 JaCoCo 原理简介

    JaCoCo 到底是怎么手机覆盖率信息的呢?

    插桩

    何谓插桩?

    用通俗的话来讲,插桩是将一段代码通过某种策略插入到另一段代码,或者替换另一段代码,来收集程序运行时的动态上下文信息。

    这里的代码既可以是字节码也可以是源码。

    JaCoCo 就是字节码插桩方式

    其中字节码插桩又分为 on-the-fly offline 的两种模式。

    3.2.1 on-the-fly 模式

    在 JVM 中通过添加-java agent 参数指定特定的 jar 文件启动 Instrumentation 的代理程序,代理程序会在 ClassLoader 装载一个 class 前判断是否已经转换修改了该文件,如果没有则将探针插入 class 文件中,探针不改变原有方法的行为,只是记录是否已经执行。

    3.2.2 offline 模式

    在测试之前先对文件进行插桩,生成插过桩的 class 或 jar 包,测试插过桩的 class 和 jar 包,生成覆盖率信息到文件,最后再统一处理,生成报告。

    3.2.3 on-the-fly 和 offline 对比

    on-the-fly 更方便简单,无需提前插桩,无需考虑 classpath 设置问题。


    以下情况不适合使用 on-the-fly 模式:

    1. 不支持 javaagent

    2. 无法设置 JVM 参数

    3. 字节码需要被转换成其他虚拟机

    4. 动态修改字节码过程和其他 agent 冲突

    5. 无法自定义用户加载类


    如下图,包含了几种不同的覆盖率信息的收集方法,其中带颜色的是 JaCoCo 比较有特色的部分:


    图片来源:官网*

    关于 JaCoCo 具体的注入原理,在官方网站上写的非常详细了,网上翻译修改的资料也非常多,这里不做过多赘述。


    经过对比,我们在统计功能测试覆盖率以及集成测试覆盖率时,选择的是 On-the-fly 模式。

    原因是 On-the-fly 方式无须入侵应用启动脚本,只需在 JVM 中通过 -javaagent 参数指定 JaCoCo 的代理程序。

    四、JaCoCo 使用方式

    4.1 Apache Ant 方式

    参考:JaCoCo Ant 方式使用

    https://www.eclemma.org/jacoco/trunk/doc/ant.html

    4.2 Apache Maven 方式

    参考:JaCoCo Maven 方式使用

    这种方式适合 maven 项目。

    https://www.eclemma.org/jacoco/trunk/doc/maven.html

    4.3 Eclipse EclDmma Plugin 方式

    参考:JaCoCo Eclipse 使用

    这种方式主要和 eclipse 集成,用户可以直观的看到覆盖率的情况。

    https://www.eclemma.org/

    4.4 命令行方式

    官方文档上详细介绍了用到的参数和用法,其主要使用如下 JVM 参数来激活 Java agent 代理:

    -javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]

    但是它也接受一些其他的参数,详情可查看官方文档。

    目前我司是使用此方式来统计代码覆盖率的。下面一起来看下具体是如何使用的吧!

    4.4.1 更改 server 的启动脚本,使用 jacocoagent.jar 启动服务;

    1. 在官网下载 JaCoCo 的 jar 包;

    2. 修改启动 server 的参数,使用 jacocoagent.jar 记录服务的操作数据,启动项增加下面内容。

    4.4.2 生成覆盖率报告;

    1. 从服务器上获取生成的 exec 文件放到部署覆盖率平台服务的机器上;

    2. 调用 JaCoCo 的 api 生成报告,exec 地址为启动脚本中 destfile 指定的文件。

    理论上不用杀 server 进程就可以直接 copy 到最新的 exec 文件,但是如果遇到报告结果是空的情况,可以考虑先 kill server 进程,再拷贝 exec 文件。

    参考官方 demo 的有具体的示例:ReportGenerator.java

    https://www.eclemma.org/jacoco/trunk/doc/examples/java/ReportGenerator.java

    部分代码如下:

    public testjacoco(final File projectDirectory ) {    this.title = projectDirectory.getName();    this.executionDataFile = new File(projectDirectory, "scfzzpostjacoco.exec");    //目录下必须包含源码编译过的class文件,用来统计覆盖率。所以这里用server打出的jar包地址即可    this.classesDirectory = new File(projectDirectory, "/");    // this.sourceDirectory =null;    //源码的/src/main/java,只有写了源码地址覆盖率报告才能打开到代码层。使用jar只有数据结果    this.sourceDirectory = new File("/opt/RD_Code/server/zhuanzhuan_scf_zzpost_4-0-38_BRANCH/service/", "src/main/java");    //coveragereport为要保存报告的地址    this.reportDirectory = new File(projectDirectory, "coveragereport");}
    复制代码

    这里主要是调用 testjacoco()方法来做入口生成报告。

    其中,

    this.title 是报告的标题;

    this.executionDataFile 是第一步生成的 exec 的文件;

    this.classesDirectory 是源码的 class 文件,只要传递 class 所在目录就可以(或者用编译过的 jar 包也可以),不传递会报错,用来统计覆盖率 ;

    this.sourceDirectory 是源码所在目录,可以不赋值使用 null,但这种覆盖率结果只有看到方法名级别,不能直接看到方法中具体的覆盖结果 。


    这一步完成之后,我们就可以在通过在浏览器查看 html 报告,来具体的分析代码覆盖率。

    五、得物 app 实践

    目前我们基于公司的业务模式,对 JaCoCo 做定制化改造,搭建了覆盖率平台,支持产出增量覆盖率和全量覆盖率报告,也支持手动更新全量覆盖率数据。


    由上文我们可以了解到,我们使用 JaCoCo 最终目的是需要生成覆盖率报告,可以拆分为以下几个步骤:

    测试完成之后生成的 exec 统计文件;

    插桩后的 classes 文件;

    本次部署的服务在 gitlab 上的代码;

    利用 JaCoCo 的 api 生成报告。


    目前覆盖率平台一期只支持全量覆盖率,是通过定时执行接口自动化任务来获取数据的。


    那具体实现流程是怎样的呢?实践过程中遇到了哪些问题,又是如何解决的呢?

    一起来了解一下吧!

    5.1 获取生成的 exec 统计文件

    首先我们要修改服务的启动脚本,带上 JaCoCo 用以插桩并监听启动参数。

    put=tcpserver,port=33511,address=10.11.22.19,append=true
    复制代码

    其中,

    /home/ops/testjacoco/lib/jacocoagent.jar 是 JaCoCo jar 包的目录;

    includes=com.sz.* 是对包进行过滤;

    output=tcpserver 表示以 tcpserver 方式启动应用并进行插桩;

    port=33511 是 jacoco 开启的 tcpserver 的端口,请注意这个端口不能被占用;

    address=*.*.*.* 是对外开放的的 tcpserver 的访问地址,可以配置 127.0.0.1,也可以配置为实际访问 ip:

    配置为 127.0.0.1 的时候,dump 数据只能在这台服务器上进行 dump,就不能通过远程方式 dump 数据;

    配置为实际的 ip 地址的时候,就可以在任意一台机器上(前提是 ip 要通,不通都白瞎),通过 ant xml 或者 api 方式 dump 数据。


    在实践的时候发现脚本里不能写死监听端口,因为服务器上启动了多个应用服务,且端口都是随机的,一旦端口被占用会导致应用启动失败。


    为了解决这个问题,我们在启动服务的时候先执行 shell 脚本获取未占用的端口,给 JaCoCo 使用。

    其次,覆盖率平台服务需要获取每个服务的端口信息。JaCoCo 自身是通过 Jsch 在应用服务器上执行命令,获取被统计应用服务所使用的 JaCoCo 端口。但是 Jsch 在只能执行单条指令,对 shell 有限制,所以我们自己编写了一个脚本,将服务信息和端口信息先输出到文件中(如下图),再将文件传输到覆盖率平台所在服务器。


       

    使用 JaCoCo 启动了服务器,拿到了端口号,第三步就是需要执行接口自动化,获取 exec 统计文件,我们通过启动定时任务,定时执行脚本,获取覆盖率数据。


    为了获取纯净的覆盖率数据,执行脚本的时间,既不能影响测试流程又需要在自动化 case 运行完成的时候统计,单纯使用定时任务并不能很好的满足目前需求。因为接口自动化 case 运行时长不定、服务部署时长也不固定。在这里我们通过记录两个运行状态,来决定统计的时间。


    在使用 scheduled 任务的时候遇到了一个问题,在定时任务类中使用 @Autowired 注解时,会报空指针异常,因为 mapper 实例化时为 null,不能调用 mapper 实现中的方法,因为 Spring 的 Schedule 是通过 Quartz 实现的,但默认时, 并不直接支持 ApplicationContext 。在实际项目中,我们用一个类实现了 ApplicationContextAware 接口。这样,这个类可以直接获取 Spring 配置文件中,所有有引用到的 Bean 对象。

    实现代码如下:

    @Componentpublic class WorkUtil implements ApplicationContextAware {    private static ApplicationContext context;    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        context = applicationContext;    }    public static ApplicationContext getApplicationContext() {        return context;    }    public static Object getBeans(String name) {        return getApplicationContext().getBean(name);    }}
    复制代码


    我们在使用时,直接调用 getBeans()方法即可获取 bean 对象。

    5.2 获取 classes 文件

    得物服务的代码,为多 moudle 模式,打 jar 包时,一些实现业务逻辑 moudle 被打成 jar 包跟一些第三方依赖包混淆在一起。而我们统计覆盖率的时候,只需要统计放真正代码逻辑的 jar 包,如果包含一些第三方依赖包含的东西太多会显得很杂乱。

    通过修改 JaCoCo 源码,在字节码处理的时做判断,将公司的业务代码从整个 project 中拆出来,然后在生成报告这步中进行聚合,生成一个较为简洁的报告。


    在最后做统计的时候,JaCoCo 需要插桩,JaCoCo 会在编译期间加入 JacocoData 成员变量,如果使用循环反射成员变量,没有做校验的情况中,代码中会多出 $jacocoData 变量,这样会影响到被统计服务运行,我们在代码中加如下的 if 判断,来检查成员变量是否有 java 编译器引入的,如果不是则跳过,继续执行原有逻辑。

    if (!(Object instanceof ColomboComponent)){        continue;    }
    复制代码


    5.3 获取 git 上的源码

    这一步主要是通过 JGit 实现的,此过程主要是将 git 上的源码同步覆盖率平台上,在平台所在服务器对所需要的的分支进行切换更新等操作。1)此过程中主要碰到过 git pull 代码 Auth fail,经检查代码账号信息没有问题,主要是连接超时会导致该问题,文件过大也会导致这个问题通过设置 git 配置解决此问题,配置如下:

    git config --global http.lowSpeedLimit 0git config --global http.lowSpeedTime 999999git config http.postBuffer 524288000
    复制代码


    5.4 生成覆盖率报告

    前面的准备工作都做好了,再生成覆盖率报告的时候发现,由于版权保护问题,修改了 JaCoCo 源码会导致服务编译不通过。梳理了代码逻辑,确认代码各个方法都没问题,最后在代码中添加了如下注释,编译成功!

    具体注释如下:

    /** * Encapsulates the tasks to create reports for Maven projects. Instances are * supposed to be used in the following sequence: * * <ol> * <li>Create an instance</li> * <li>Load one or multiple exec files with * <code>loadExecutionData()</code></li> * <li>Add one or multiple formatters with <code>addXXX()</code> methods</li> * <li>Create the root visitor with <code>initRootVisitor()</code></li> * <li>Process one or multiple projects with <code>processProject()</code></li> * </ol> */
    复制代码

     

    在平台化的过程中,大部分的时间都放在了修改 JaCoCo 源码上。


    因为 JaCoCo 源码中,用到的全局变量比较多,如果不从头开始读,无法发现这个变量是什么时候被赋值的,代码中也会包含以下类似于 s1、s2 这中变量名,读起来比较费劲,只能通读整个代码,同时也有助于我们更深入的了解了 JaCoCo 的实现。


    我们也在不断的对平台的功能进行完善。除了二期实现增量覆盖率功能以外,后续也会将覆盖率的统计功能做的完善一些。在积累数个版本的全量覆盖率之后,可以建立预警机制,输出每日开发自测、测试人员手动测试、自动测试覆盖率,分析合理的增长趋势。如果偏离该趋势,则及时进行预警。也可以和用例平台打通,获取到每条用例覆盖到的函数和影响到的接口,通过更小的维度更精准地度量测试质量。

    六、总结

    通过引入代码覆盖率分析体系,我们可以精确把控增量代码质量,持续改善优化存量代码。同时也可以将测试用例的影响范围细化到代码层面,从而实现精准化测试。


    但是,盲目的追求代码覆盖率是没有意义的,即使已经达到了 100%的代码覆盖率,软件的质量也不可能做到万无一失,因为代码覆盖率的计算是基于现有代码的,并不能发现那些「未考虑某些输入」以及「未处理某些情况」形成的缺陷


    并且在追求更高的代码覆盖率时,我们需要补充更多的 case,去覆盖更多的代码,这样测试成本也会以指数级的方式迅速增加,花费的时间成本会也会更高。


    所以,我们在实际工作中,需要正确恰当地应用代码覆盖率,使其能够帮助我们更精准地定位和分析问题,保证产品质量,为精准测试添砖加瓦,发挥它的最大价值。


    文/Snowdreams

    关注得物技术,带你走向技术的云端


    发布于: 2021 年 01 月 15 日阅读数: 36
    用户头像

    得物技术

    关注

    得物APP技术部 2019.11.13 加入

    关注微信公众号「得物技术」

    评论

    发布
    暂无评论
    【得物技术】代码覆盖率原理与得物app实践