腾讯移动品质中心TMQ [腾讯 TMQ] JAVA 代码覆盖率工具 JaCoCo-实践篇

匿名 · 2016年08月30日 · 最后由 niu 回复于 2017年08月11日 · 6701 次阅读

作者:刘洋

一、覆盖率项目中使用介绍

本节开始详细介绍下项目中的 JaCoCo 实战经验。

下图是覆盖率在实际在项目中的主要实施点:

分别详细介绍下:

1.1 确定插桩方式

Android 项目只能使用 JaCoCo 的离线插桩方式。

为什么?主要是因为 Android 覆盖率的特殊性:

一般运行在服务器 java 程序的插桩可以在加载 class 文件进行,运用 java Agent 的机制,可以理解成"实时插桩"。JaCoCo 提供了自己的 Agent,完成插桩的同时,还提供了丰富的 dump 输出机制,如 File,Tcp Server,Tcp Client。覆盖率信息可以通过文件或是 Tcp 的形式输出。这样外部程序可很方便随时拿到被测程序的覆盖率。

但是 Android 系统破坏了 JaCoCo 这种便利性,原因有两个:

(1)Android 虚拟机不同与服务器上的 JVM,它所支持的字节码必须经过处理支持 Android Dalvik 等专用虚拟机,所以插桩必须在处理之前完成,即离线插桩模式。

(2)Android 虚拟机没有配置 JVM 配置项的机制,所以应用启动时没有机会直接配置 dump 输出方式。

1.2 分析项目打包流程

项目目前还是已 build 方式打包,属于 Apache Ant 方式。

插桩前先熟悉下项目 build 内容。

项目主要有几个 build 文件:

存放在根目录下的 build.xml 文件,这个是项目构建的组织文件

  • ant 目录下的 build_common.xml,这个是构建时 target 内容

  • ant 目录下的 build_option.xml,定义的属性文件。

  • ant 目录下的 build_plugins.xml,插件文件。

在插桩前,应该对项目构建过程做一个总体的熟悉,了解下每个 target 的作用,这样才能确定不会影响各个插桩点,不会遗漏,否则会在打包的过程中出现各种各样的问题。

1.3 代码插桩

http://eclemma.org/jacoco/trunk/doc/ant.html,这个地址是 JaCoCo 的 ant 的说明文档。

里面简单介绍了其支持的 task 类型,包括:
Task coverage、Task agent、Task merge、Task report、Task instrument、Task dump

具体怎么使用可以参考里面的例子。

各 Task 实际调用的类,看一下 JaCoCo 的 antlib.xml 就知道了


项目根据自己的情况暂时只用到了 Task instrument,其他 dump、merge、report 是通过其他方式使用的,具体后面有说明。

为什么没有用到 dump、merge、report?

这种情况比较适合一个带有自动化测试的构建:打包、自动化测试、dump、merge、report。

项目部分功能需要手工测试,因此,上述几个步骤需要后面再另外处理。
OK,简单了解了 JaCoCo 的 ant 方式,下面开始对项目进行插桩打包。

项目的插桩修改步骤:

主要修改了 build-common.xml 和 build-plugins.xml 两个文件:
以下是 build-common.xml 的修改,build-plugins.xml 的修改就不累述了,原理一样。

(1)文件开头的命名空间加入
xmlns:JaCoCo="antlib:org.JaCoCo.ant"

(2)引入 JaCoCo 的 jar 和相关定义

`

`

(3)重新定义 class 文件生成路径
<property name="classes_instr" value="${temp}/classes_instr" />

(4)修改 compile 编译节点,插桩注入
`

/JaCoCo:instrument`

(5)修改打包 package 节点,主要是指定 JaCoCo 编译后的类路径
<jar basedir="${classes_instr}" destfile="temp.jar" />

(6)修改混淆 obfuscate 节点,增加混淆所需要的

<arg value="-libraryjars ${lib}/JaCoCoagent.jar" />
将delete、mkdir、unzip操作指向classes_instr

(7)修改分包 splitClasses 节点,指向 classes_instr

<arg value="${classes_instr}" />

(8)修改热补丁注入 injectPatchCode 节点,指向 classes_instr

<YYBInjectPatchCode inputDir="${classes_instr}"

(9)修改 dex 节点,指向 classes_instr

 <arg path="${classes_instr}"

(10)修改 dex-sub 节点,指向 classes-instr,同时在 excludes 中加入 jacocoagent.jar
<arg path="${classes_instr}"
fileset dir="${lib}" excludes="tmdownloadsdk.jar,tmapkpatch.jar,.....,jacocoagent.jar" />

将上面的操作,做成全自动修改,打包成 autoinsertxml.jar,放到打包服务器后台指定的目录下。

Jar 包里详细内容如下:

  • 修改 AndroidManifest.xml 文件,增加一个覆盖率生成服务(这个后续的覆盖率生成工具用到)

  • 修改 build_common.xml 文件,实现主干代码插桩修改

  • 修改 build_plugins.xml 文件,实现插件代码的插桩修改

1.4 打覆盖率包

Jekin 上已经配置好了 jacoco_package 任务

按描述输入后,直接点击开始构建就行了,打包后的结果:

包括:未插桩的主干类文件、未插桩的插件类文件、三种方式的覆盖率包、mapping 文件等等。

jacoco_package 任务里面的具体内容做了什么?一起看看吧。

(1)配置了参数化构建的内容,如

(2)配置了构建描述

(3)配置了项目 ID 和创建精准入库任务

(4)Check out 代码

(5)插桩

(6)编译打包

(7)备份 class

(8)保存存档文件

1.5 执行测试,收集覆盖率结果文件

覆盖率文件生成现在支持两种方式:

1、覆盖率生成工具:一个专门用来生成覆盖率文件的 APK。

2、定时器的方式:在项目里新建一个定时器 JOB 任务,定时去收集生成覆盖率文件。

目前我们主要用第一种方式,下面都详细介绍下。

1.5.1 AndroidManifest 文件的修改

增加了两个服务:

ResultManagerService:执行生成覆盖率数据。

ReSetManagerService:执行清理覆盖率数据。

1.5.2 生成覆盖率的 apk 工具和 jacoco-cov-sdk.jar 包

工具总共有三个功能:

(1)生成 ec 文件

(2)启动定时器,按指定的时间生成 ec 文件

(3)清除覆盖率,会清除内存记录并且会删除 sd 卡存在的 ec 文件

工具原理:

(1)生成 ec 文件

当触发这个操作的时候,其实会去启动项目中我们添加的 ResultManagerService 服务,它具体做的事情就是 dump 覆盖率数据,如下:在 ResultManagerService 启动时调用 jacoco-cov-sdk.jar 包中的 ResultManager.dumpCoverageJacoco(true,filename) 方法:

其主要功能就是反射调用 jaCoCo 的 dump 方法,来生成覆盖率数据,核心代码如下:
`//Get AgentOptions class
Class classAgentOptions = Class.forName("org.jacoco.agent.rt.internal_b0d6a23.core.runtime.AgentOptions");

//Get setDestfile method in AgentOptions class
Method methodSetDestFile = classAgentOptions.getMethod("setDestfile",String.class);

//Get FileOutput class
Class classFileOutput = Class.forName("org.jacoco.agent.rt.internal_b0d6a23.output.FileOutput");

//Get field "File destFile" in FileOutput class

Field fieldFile = classFileOutput.getDeclaredField("destFile");

fieldFile.setAccessible(true);

//Get Agent singleton by getAgent method in RT class
Class<?> RT = Class.forName("org.jacoco.agent.rt.RT");

Method methodGetAgent = RT.getMethod("getAgent");

Object objAgent = methodGetAgent.invoke(null);

//Get Agent Class

Class classAgent = Class.forName("org.jacoco.agent.rt.internal_b0d6a23.Agent");

//Get field "AgentOptions options" and "FileOutput output" in Agent Class

Field fieldOptions = classAgent.getDeclaredField("options");

Field fieldOutput = classAgent.getDeclaredField("output");

fieldOptions.setAccessible(true);

fieldOutput.setAccessible(true);

//Get options/output object referenced by Agent singleton

Object objOptions = fieldOptions.get(objAgent);

Object objOutput = fieldOutput.get(objAgent);

//change destFile attribute in options object by setDestfile method

methodSetDestFile.invoke(objOptions,absFilePath);

//change field "File destFile" in output object

File destFile = new File(absFilePath).getAbsoluteFile();

fieldFile.set(objOutput,destFile);

//dump

Method methodDump = classAgent.getMethod

("dump",boolean.class);

methodDump.invoke(objAgent,reset);`

(2)启动定时器,按指定的时间生成 ec 文件

这个就是一个 Timer,按指定的时间周期去 dump 覆盖率数据

(3)清除覆盖率,会清除内存记录并且会删除 sd 卡存在的 ec 文件

当触发这个操作的时候,其实会去启动项目中我们添加的 ReSetManagerService 服务,它具体做的事情就是 reset 覆盖率数据,如下:

在 ReSetManagerService 启动时调用 jacoco-cov-sdk.jar 包中的 ResultManager.reSetCoverageJacoco() 方法:

其主要功能就是反射调用 jaCoCo 的 reset 方法,来清理覆盖率数据,核心代码如下:

Class<?> RT = Class.forName("org.jacoco.agent.rt.RT");
Method methodGetAgent = RT.getMethod("getAgent");
Object objAgent = methodGetAgent.invoke(null);
//Get Agent Class
Class classAgent = Class.forName("org.jacoco.agent.rt.internal_b0d6a23.Agent");
//reset
Method methodDump = classAgent.getMethod("reset");
methodDump.invoke(objAgent,null);

1.6 生成覆盖率报告

通过编写 report 的 build 方式来生成报告结果。

这里写了一个生成报告的模版,使用者只需要 copy 到 本机上,按下面的说明修改、生成报告即可,下面详细介绍下这个模版的使用方法。

1.6.1 模版目录介绍

(1)libs 存放几个 jar 包,分别为 ant-contrib.jar、jacocoagent.jar、jacocoant.jar;

(2)result_xml 目录会自动生成 xml 格式的报告;

(3)src 目录是存放源码的,如果没这个,生成的覆盖率只有数据,看不到代码实际覆盖的内容;

(4)build 文件,ant 的执行内容,build_group 或者是 build_only 的 copy。

文件内容如下:

<project xmlns:jacoco="antlib:org.jacoco.ant" name="Example Ant Build with JaCoCo Offline Instrumentation" default="build_all">

说明:指定 xmlns:jacoco、name 和缺省的执行 task

<taskdef resource="net/sf/antcontrib/antcontrib.properties">
<classpath>
<pathelement location="./libs/ant-contrib.jar"/>
</classpath>
</taskdef>

说明:引入了 ant 的 jar 包,放入到 classpath 中
<import file="build_property.xml"/>

说明:import 了属性文件

<property name="result.dir" location="."/>
<property name="src.dir" location="./src"/>
<property name="result.classes.dir" location="${result.dir}/classes/classes"/>
<property name="result.report.dir" location="${result.dir}/report"/>
<property name="result.report.xml" location="${result.dir}/result_xml"/>

说明:定义了一下属性:源码路径、类路径、报告目录、xml 目录

<!-- Step 1: Import JaCoCo Ant tasks -->
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="./libs/jacocoant.jar"/>
</taskdef>

说明:引入 jacocoant.jar,指定了 ant 的操作映射文件

<target name="build_all">
<foreach list="${ec_list}" target="report" param="ec_name" delimiter=","/>
</target>

说明:build_all 任务的内容,循环遍历 ec_list,作为 report 的输入

`


/jacoco:merge
jacoco:report

<!--file file="${result.dir}/${ec_name}.ec"/-->







<!--fileset dir="${result.dir}/src/libs">



















/jacoco:report
`

说明:report 任务的内容,指定 merge、指定 ec 文件、结果已 YYB 组显示、指定 classes 文件、指定 src 文件列表、指定 html、csv、xml 属性。
</project>

说明:这个 build 文件需要根据实际的项目修改,修改一次以后基本不用变动。

(1)build_group 文件,指定组生成的 build 文件,适合结果按组显示。

(2)build_only 文件,没组的概念。

(3)build_property 文件,存放的是 ec 列表文件名称。

1.6.2 实际操作举例

一、安装 Apache Ant

网上下载或直接 copy 其他人机器上的就 OK,设置下环境变量 ANT_HOME 和把 bin 目录放到 path 中,我用的是 apache-ant-1.9.6-bin,有需要可以直接找我要。

二、取上面的模版目录放到你本机上 (PC),有需要可以直接找我要。

三、生成报告,按以下步骤操作

比如拿到测试结果的 ec 文件有三个,分别是 yyb1.ec、yyb2.ec、yyb3.ec

1、将覆盖率打包结果中的 classes.zip 丢到模版根目录中并解压。

2、根据打包时的 svn 地址和版本号,取下源码放如到 src 目录。

3、将 ec 文件 (yyb1.ec、yyb2.ec、yyb3.ec) 全部丢到模版根目录中

4、修改 build_property 文件,名称写如到 value 中(去掉 ec 后缀的)

5、build 文件,如无路径变化,基本不用修改
省电管理除了主干代码,还有插件部分,因此 build 文件取的 build_group,分别为

<group name="YYB">、<group name="plugin_power_save">

6、执行 ant,report 目录就会生成。

7、report 目录生成后,进去执行 index 就看到覆盖率报告。
省电管理的覆盖率生成结果:

打开 index 后的结果,按 build 文件指定的分组生成了:

进入到实际代码中的结果:

这里有个注意的地方,如果想看到实际代码的覆盖率,编译的时候 debug="true" 这个一定要设置。

###1.7 分析覆盖率结果
网上关于 JaCoCo 覆盖率报告的分析有不少的文章可以学习。

这里阐明几个自己的观点:

根据项目的不同,在分析结果前先应该明确几个事情,包括

(1)确定改动点的范围,根据这个范围才会有针对性的做分析。

(2)改动点是否影响功能逻辑,如果不影响可以忽略。

(3)改动点和其他功能是否存在耦合,如果存在,耦合的部分也要做分析。

我们主要从上面几点来分析覆盖率,查漏补缺,这些改动点大部分已经覆盖到了,基本认为应用的主要功能覆盖完全,当然也不是完全绝对,在测试过程中结合 FreeTest、探索性测试等手段也是一种不错的选择,切记不要盲目的为了覆盖率而覆盖,覆盖率高不代表你真的覆盖完全了。

分析过程很多人觉得是比较痛苦的,不妨可以把这个过程当作是一种锻炼,前面的一切都只是一个铺垫,最最关键的在于分析阶段,一个出色的分析结果可以达到事半功倍的效果。

我们的方法是任务已 tapd 提单的方式创建,按照模版,附上需求链接地址、svn 地址和改动范围、附件接受未插桩的 class 文件、测试后的 ec 文件等,分析出结果需要有分析过程、测试补充建议、分析耗时等等。

主要列出未覆盖场景、冗余方法、测试补充建议等等。

举个分析的例子:

需求: 消息盒子增量测试完成,进行覆盖率分析。
####1.7.1 熟悉需求用例

1、确认代码范围


2、覆盖率报告分析

3、确认未覆盖原因

4、输出测试策略

5、补充测试验证

二、覆盖率与 BVT 测试结合

通过两者的结合,可以得到每个 BVT 的用例的覆盖率数据,可以得出几个纬度的结果:

(1)用例和代码的对应关系

用例和代码的动态映射关系,可能会存在映射到的函数比较多,作者建议根据功能有针对的筛选出重点函数来做映射。

(2)上面映射关系汇总后,可以按方法的调用频繁度来优化我们的代码,
优化调用频繁度高的代码,找出冗余代码等等。

下面介绍下整个过程:

2.1 在 BVT 用例框架中插入覆盖率方法

核心:找出关键点插入我们的覆盖率方法

(1)在每个用例执行前,插入清理覆盖率数据的方法

在 BVT 基类的 setUp() 方法最后插入清理覆盖率数据的方法。

@Override
protected void setUp() throws Exception {
super.setUp();
//反射调用JaCoCo api的reset方法
...
Method methodDump = classAgent.getMethod("reset");
methodDump.invoke(objAgent,null)

这样每个用例开始执行前,就会把以前遗留的覆盖率数据清除掉,保证每次覆盖率都是一条用例的执行结果。

(2)在每个用例执行后,dump 出覆盖率数据。

@Override
protected void tearDown() throws Exception {
//反射调用JaCoCo api的dump方法
...
Method methodDump = classAgent.getMethod("dump",boolean.class);
methodDump.invoke(objAgent,reset);
}

dump 出来的数据用例执行过程中真实的覆盖率情况。

Method methodDump = classAgent.getMethod("reset");
methodDump.invoke(objAgent,null)

2.2 执行 BVT 用例,得到覆盖率

运行 BVN 的用例,用例执行成后输出覆盖率文件,一条用例对应一个覆盖率文件。

2.3 批量生成覆盖率报告,解析入库

批量生成覆盖率报告,根据用例和报告对应关系做批量入库。

2.4 分析覆盖率结果,得出用例和代码映射关系

上面我们已经得出每一个 BVT 用例的覆盖率数据,对每一个覆盖率数据结果进行分析,得出几个纬度的数据,用例->包->类->方法的覆盖数据,这样每个用例和代码的映射关系就出来了。

然后根据用例对应功能的特点,再筛选出重点方法,形成一个比较精简的用例和代码映射关系出来,方便我们后续的改动点定位。

三、差异覆盖率和全量覆盖率

测试完后,根据覆盖率结果衡量测试覆盖程度,主要分为两种:

(1)差异覆盖率:改动点的代码执行覆盖率情况

(2)全量覆盖率:本次测试代码执行全部覆盖率情况

使用哪种覆盖率是由测试阶段的内容决定,比如上线前测试、集成或合流阶段,主要关注的是改动点的变化,使用差异覆盖率效果比较理想。如果是新增功能,使用全量覆盖率比较理想。

3.1 差异覆盖率

差异覆盖率主要是根据开发代码变更的 diff 差异,得出改动代码的范围,然后根据这个范围有针对性的只生成这部分改动的代码覆盖率结果。

通过覆盖率结果反向衡量测试的充分性,更好的和精准评估的测试范围去做比较。

3.2 全量覆盖率

全量覆盖率即全部代码的覆盖结果,不一定要全部去分析,只需关注改动部分及其耦合功能的覆盖情况即可,这里结合精准耦合分析结果一起分析。

四、衡量覆盖率结果

代码覆盖是一种状态指示器,而不是衡量性能或正确性的单元。

代码覆盖率是给程序员参考的,是给我们发现代码中问题的一种手段,可以发现过时的,未测试的类,还可以发现未经测试执行可能导致问题的路径。在实际项目中,代码覆盖率总是低于 100%。取得完全覆盖是不可能的,如果取得,那也是非常罕见的。分析前一定要确定那些为必须覆盖,那些为可以或不覆盖,不要为了覆盖而覆盖,代码逻辑的熟练程度对分析覆盖率会有很大的帮助,一定要先梳理清楚。

五、本章小结

代码覆盖率是软件测试中的一种度量手段,主要用来描述程序中源代码被测试的比例和程度。

在单元和系统测试过程中,其常常被拿来作为衡量测试好坏的指标,甚至很多情况下用代码覆盖率来考核测试任务完成情况,经常会被要求代码覆盖率必须达到 XX% 以上,才算测试充分,于是乎测试人员或者开发人员费尽心思设计案例来覆盖代码,这种用代码覆盖率来衡量,有利也有弊。

给读者的一些忠告:
1、覆盖率数据只能代表你测试过哪些代码,不能代表你测好这些代码。
2、不要过于相信覆盖率数据。
3、不要只拿语句/行覆盖来衡量
4、路径覆盖率>判断覆盖>语句覆盖
5、不要盲目的为了提供覆盖率而补充用例,应该想办法设计更好的用例,哪怕多设计的用例对覆盖率提升没有效果。

本章完~

原文链接:enter link description here


TMQ(腾讯移动品质中心)是腾讯最早专注在移动 APP 测试的团队
我们专注于移动测试技术精华,饱含腾讯多款亿级 APP 的品质秘密,文章皆独家原创,我们不谈虚的,只谈干货!

扫码关注我们

扫一扫 关注 TMQ
精彩分享不断
共收到 4 条回复 时间 点赞

您好,代码覆盖率能具体到我哪些功能模块是否测试到吗?

楼主你好,最近在用 jacoco 生成报告的时候,只能看到方法目录,看不到源码(就是说具体覆盖了哪些),请问您知道如何解决这个问题吗?

niu 回复

我也遇到看不到源码的问题,请问找到原因了吗

gb2312 回复

源码和 class 文件路径不匹配,如果完全匹配就可以看到

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册