代码覆盖率原理简介

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

 

 

代码覆盖率简述

 

什么是代码覆盖率

 

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

 

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

 

代码覆盖率的价值

 

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

那么,代码覆盖率到底能给我们带来怎样的价值呢?

 

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

 

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

 

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

 

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

 

常用指标

 

  • 语句覆盖

    又称行覆盖(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测试到就可以了。

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

 

JAVA覆盖率工具介绍

 

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

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

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

 

关于JaCoCo

 

JaCoCo简述

 

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

(官方文档地址:

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

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

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

 

JaCoCo原理简介

 

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

插桩

 

何谓插桩?

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

 

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

 

JaCoCo就是字节码插桩方式

 

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

 

on-the-fly模式

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

 

offline模式

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

 

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使用方式

 

Apache Ant方式

 

参考:JaCoCo Ant方式使用

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

 

Apache Maven方式

 

参考:JaCoCo Maven方式使用

这种方式适合maven项目。

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

 

Eclipse EclDmma Plugin方式

 

参考:JaCoCo Eclipse使用

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

https://www.eclemma.org/

 

命令行方式

 

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

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

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

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

 

更改server 的启动脚本,

使用jacocoagent.jar启动服务

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

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

    (更多参数请看官网)-

    javaagent:/tmp/testjacoco/lib/jacocoagent.jar=includes=com.*.*,output=tcpserver,port=39512,address=服务器ip,append=true

    系统在jvm停止的时候会dump覆盖率信息,存储在exec文件中。

 

生成覆盖率报告

  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最终目的是需要生成覆盖率报告,可以拆分为以下几个步骤:

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

  2. 插桩后的classes文件;

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

  4. 利用JaCoCo的api生成报告。

     

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

 

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

一起来了解一下吧!

 

获取生成的exec统计文件

 

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

  •  
javaagent:/home/ops/testjacoco/lib/jacocoagent.jar=includes=com.sz.*,output=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对象。

 

获取classes文件

 

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

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

 

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

 

if (!(Object instanceof ColomboComponent)){        continue;    }

 

获取git上的源码

 

这一步主要是通过JGit实现的,在git pull代码时会出现Auth fail的错误,经排查发现是连接超时或者文件过大导致的。最后在服务器上修改git配置,解决了这个问题。

具体设置如下:

​​​​​​​

git config --global http.lowSpeedLimit 0git config --global http.lowSpeedTime 999999git config http.postBuffer 524288000

 

生成覆盖率报告

 

前面的准备工作都做好了,再生成覆盖率报告的时候发现,由于版权保护问题,修改了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,去覆盖更多的代码,这样测试成本也会以指数级的方式迅速增加,花费的时间成本会也会更高。

 

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

 

公众号原文:https://mp.weixin.qq.com/s/FMWfE76WyLvJVidIjNCLcg

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值