测试覆盖率 小记 Java 服务端和 Android 端手工测试覆盖率统计的实现

风雨夜阑听 · September 19, 2019 · Last by jacky replied at February 20, 2021 · 5663 hits

关于后端和 App 端手工测试覆盖率的总结

一、前言

代码覆盖率工具已经流行了这么多年,其他公司已经用熟、用烂了,我司近一段时间开始了这方面的摸索,很荣幸这个任务到了我的手里,于是乎就开始踩坑之旅。

之前已经搞定了 Java 后端的覆盖率统计,当然了我们没有 UT,毫无疑问使用的还是 On-the-fly 模式,最近几天开始了 App 端的手工测试覆盖率统计之旅,中间出现了一些坑,有的很快就搞定了,有的也多多少少占用了不少时间,在此记录一下,免得以后忘记,也抛砖引玉,一起探讨下。

二、前期思绪整理

2.1 关于 Java 端

我们都知道,jacoco 流行了这么多年,无疑是解决 Java 后端覆盖率的不二之选 (至于 Emma,当然也是先驱,只不过我们没有选择)。分析了我司关于 Java 端项目的现状,总结了如下几点:

  • 老牌的项目,使用 Java6,框架么就是 ssh 咯,一般来说都是 Ant 构建,jetty 部署
  • 稍微新一点的项目,使用 Java8,主框架 SSM,一般直接 maven 构建,maven 的 tomcat 插件部署或者打成 war 包用 tomcat 部署
  • 再新一点的项目,使用 Java8,就是 Springboot 和 Spring Cloud 框架,maven 的 springboot 插件部署或者 war 包用 tomcat 部署,或者打成 jar 包,直接用 Java 命令启动

终其一点,还是要用 On-the-fly 模式,方便嘛

2.2 关于 Android 端

基本清一色 Android Studio 开发,gradle 构建打包

2.3 关于覆盖率环境的部署和收集

研究过 jacoco 的都知道,它支持多种方式的部署和报告生成。但总结到一点,还是修改启动时候的 jvm 参数,-Javaagent 配置一下,但是不同的构建工具有其封装方式,Ant 和 Maven 也是 shell 或者 bat 脚本,对一系列操作的封装,底层依然是 Java 命令的调用。

所以一个万能的方式,就是修改 ant 和 mvn 的脚本,直接修改其脚本中用到的 JAVA_OPTS 参数,但是弊端也很明显,就是会对所有的 ant 和 mvn 命令生效,因此并不可取。
所以,还是选择了针对特定构建和打包方式的启动适配。
比如:

  • Ant 启动

可以修改 build.xml,在启动部署的 target(比如我司是用 startJetty) 中配置一个 jvmarg,设置成需要启动的 jacoco 配置,如下所示:

<target name="startJetty">
    <mkdir dir="../logs" />
    <mkdir dir="../heapdump" />
    <java classname="com.tianque.JettyProduction" spawn="true" classpath="${classes.dir}" classpathref="all.lib" fork="true">
        <arg value="${port}" />
        <arg value="${listenerport}" />
        <arg value="${path}" />
        <arg value="${rootdir}" />
        <arg value="${openJob}" />
        <jvmarg value="-Javaagent:/home/admin/jacocoagent/jacocoagent.jar=includes=*,output=tcpserver,port=8888,address=192.168.1.105" />
  • Maven 启动

mvn 启动的时候,看过 mvn 脚本的大概知道,他提供了一个 MAVEN_OPTS 这个环境变量,可以临时修改启动参数,因此对 mvn tomcat7:run 或者 mvn springboot:run 这种方式部署项目,可以选择临时修改它,来完成 jacoco 的注入,如下所示:

export MAVEN_OPTS="-Javaagent:$jacocoJarPath=includes=*,output=tcpserver,port=2014,address=192.168.110.1"

之后,在运行 mvn xx:run(需要后台启动的话,加上 nohup 也无可厚非),这样基本可以注入 jacoco agent 了。
部署之后,再设置

export MAVEN_OPTS=""

就可以恢复,或者换个 terminal 窗口,也可以恢复,这样不用动任何的后端代码,也不会对当前的服务器环境造成污染,开发在测试环境部署的时候,有很多会喜欢这种方式。

  • jar 启动

这个就更简单了,因为这个是最原始也是直接的 Java 应用启动方式

Java -Javaagent: $jacocoJarPath=includes=*,output=tcpserver,port=2014,address=192.168.110.1 -jar  xxxxxxxxxx.jar

此处需要注意-Javaagent 这个参数的放置位置,因为放在.jar 包之前是针对 jvm 设置,放到.jar 之后是针对 jar 包里启动的 main 方法 args 的参数,位置放错,就会注入 jacoco 失败

  • tomcat 启动

这种方式,就是改变 tomcat 的启动文件,catalina.sh 或者 catalina.bat 中的 JAVA_OPTS 参数,这个网上文档有很多,不再多说

2.4 关于覆盖率数据的收集和报告生成

这也有很多方式,比如用 ant 的 build.xml 和 maven 的 jacoco 插件来收集和生成报告,这个网上也有很多,不再多议。

但其实看过官方文档的就知道,其实 jacoco 自己提供了 api,来收集和生成报告,这是比较原始的方式,也是最好用的方式。

Jacoco 给出的 Api 示例如下:

Jacoco 官方的 Api 示例
地址: https://www.jacoco.org/jacoco/trunk/doc/api.html

为了降低测试部对这些知识的学习成本,我选择了统一的方式,用 api 来收集和生成报告,这样以来,测试人员需要介入的地方就只剩下了带有 jacoco agent 的测试环境部署,剩余的事情,交给我就行了。

因此,花了点时间做了个覆盖率的收集和生成报告的平台,这个后面会简单说下,不是为了炫耀也不是为了拿绩效,就是单纯想减少学习成本 (这样大家就不会抱怨还要学习 ant 的知识啦、maven 的知识啦、可能还需要学习 Java 的知识啦,虽然这是好的,但是我相信还是有很多人不愿意投入的)

2.5 关于 App 端覆盖率的收集和报告生成

关于 App 端的覆盖率收集和报告生成,社区有很多帖子介绍,我稍后也会提到,给我帮助很多。

主要思路,就是先拿到手工测试的覆盖率数据,因为这里是用 offline 的方式生成的,这一点好像途径并不多,但比较麻烦,还是要懂 Android 端的开发知识 (比如 AS 的使用、gradle 的配置和任务执行、Android 工程代码的结构、甚至还要懂一些 Groovy 的语法等等),因为要涉及到对工程的一部分修改和打包。

拿到覆盖率数据之后,就可以生成报告了,那按照之前的说法,也有两种方式:

  • 使用 gradle 的 jacoco plugin,它给出了生成报告的任务,这些涉及到的帖子里都有
  • 有了前面的覆盖率收集和报告生成平台,既然收集不需要了,那么报告生成是可以复用的嘛,于是乎做了下对 Gradle 工程的适配,只要上传 app 端的 exec 文件,就可以生成报告。

我选择了后者,还是那句话,因为如果用前者,那势必要给测试人员讲解 Android 开发一些相关知识 (生成报告的时候,还要涉及到源码和 class 文件的配置,以及涉及到多模块的收集配置)

三、感谢

在摸索的过程中,感谢以下文章给了灵感和指引,前人的摸索和努力,在很大程度上减少了我们踩坑的概率。

Android 手工测试的代码覆盖率
地址: https://testerhome.com/topics/2510

浅谈代码覆盖率
地址: https://testerhome.com/articles/16981

Android 手工测试代码覆盖率增强版
地址:https://testerhome.com/topics/2524

Jacoco 统计 Android 代码覆盖率 [instrument 方式]

地址: https://testerhome.com/topics/16376

Android jacoco 代码覆盖率测试入门

地址: https://testerhome.com/topics/17066

当然了还有很多,就不在此一一列举了,感谢社区有这么多的好文章,谢谢大家的默默付出!

[定制触发条件] jacoco 统计 Android 代码覆盖率

地址: https://testerhome.com/articles/17546

最最值得感谢的是社区的 [AngryTester]
他有个开源项目,地址如下:
https://github.com/AngryTester/jacoco
我做的平台,集成了这个开源项目,当然了也做了不少改动,但还是站在了巨人的肩膀上。在搞后端覆盖率的时候,当时也碰到很多问题,也跟这位大佬邮件交流了几次,人很 nice,回答的也很仔细,给了我莫大的帮助!感谢~~~

再次感谢这些默默付出的人们!!!

四、App 端覆盖率进行时遇到的坑

按照上述的这些精彩文章里,对 Android 端的代码覆盖率统计的介绍里,照理来说应当一气呵成了,但还是遇到了一些坑。

比如:

在收集到手机端的覆盖率数据之后,传到后台,开始生成的时候,一直报以下错误:


[2019.09.17 14:45:59][ERROR] c.a.p.t.j.ReportGenerator - IO异常,Cannot read execution data version 0x1006. This version of JaCoCo uses execution data version 0x1007.
[2019.09.17 14:45:59][ERROR] c.a.p.e.GlobalExceptionHandler - 
com.administrator.platform.exception.base.BusinessValidationException: 覆盖率的Jacoco版本不匹配:Cannot read execution data version 0x1006. This version of JaCoCo uses execution data version 0x1007.

经过分析,这是覆盖率数据和当前所用生成报告的 jacoco 版本不一致,我用的是 0.8.1-snapshort 版本,它支持的版本是 0X1007,这个从 jacoco 的源码可以看到:

很显然,是 App 端生成的覆盖率数据版本低了。

可是按照上面的文档,jacoco 中的 toolVersion 已经设置成 0.8.1 了啊,它肯定支持的也是 0X1007 啊。

而且查了官方给出的文档,对应关系如下:

地址在: https://github.com/jacoco/jacoco/wiki/ExecFileVersions

五、这个坑是怎么过去的

截止到此时,坑已经出现了,那肯定要填,于是乎就开始了一系列排查问题过程:

5.1 首先怀疑的是哪个 toolVersion,于是乎做了以下尝试

第一步,改版本 - 往高了调和往低了调都不行,依然是无法解析
第二步,把这个版本删除 (设置为空字符串"") - 我去,覆盖率数据依然能出来,这说明这个 plugin 中设置的这个 toolVersion 对这个 apk 生成的时候不生效啊。
第三部,把那个 testCoverageEnabled 设置成 false,很好,报错了,ClassNotFound,这就对了,于是乎有了后续的步骤

5.2 找了移动端的开发负责人

询问这个 jacoco 的机制,结果跟社区的文章里和网上能查到的大都差不多。
但是他给了另一个提示:[我们这个 jacoco 插件当前的运行方式是编译期,而不是运行期],
因为在此之前,我跟他描述了我们的需求,基本确定了我们的生成方式,是运行期而不是编译期,既然是在 apk 的代码里可以用反射查找到,那说明
jacoco 的代码库,确实被打到了 apk 里面

5.3 换 AS 版本

原来用的 181,改成了最新的 193,没有解决。

5.4. 换 gradle 版本

原来我们工程内置的是 4.1 版本,换成了其他版本,有的太高了根本无法构建,有的 4.4 4.6 的基本还是同样的问题

5.5 于是乎有了另外的思路,我去查它依赖的库

从 task 里,找到了 dependencies 这个任务,运行了下,
发现确实依赖的是 0.7.4.201502262128

```java
| +--- project :message
| | +--- org.jacoco:org.jacoco.agent:0.7.4.201502262128
| | +--- project :user ()
| | --- project :coreLibrary (
)
| +--- project :highlights
| | +--- org.jacoco:org.jacoco.agent:0.7.4.201502262128
| | +--- project :user ()
| | +--- project :comment (
)
| | --- project :coreLibrary (*)
| +--- project :search
| | +--- org.jacoco:org.jacoco.agent:0.7.4.201502262128

```

而且解析 apk,发现确实 apk 里面集成的也是这个版本
如图所示:

这就更加确定了,这个版本,其实目前来说没有受我们当前代码的控制, 继续从 build.gradle,找到其他依赖的库,挨个查看,主要找的是根目录下面的依赖,
在找了数十个依赖都无果之后,让我发现了这个库:

classpath 'com.android.tools.build:gradle:3.0.1

我们的代码当前引的是 3.0.1 版本,于是乎在 Maven 中央仓库中去找了下看看,地址如下:
https://mvnrepository.com/artifact/com.android.tools.build/gradle/3.0.1

它引用了一个编译期依赖: com.android.tools.build:gradle-core:3.0.1,地址如下:
https://mvnrepository.com/artifact/com.android.tools.build/gradle-core/3.0.1
如下图所示:

哇哦,发现了新大陆,它里面引用了 jacoco 的库,也确实是 0.7.4.201502262128 版本,后面的事情就没有任何选择滴开始了,升级这个版本!!!!

六、升级版本做了什么

可全局查找该属性所在位置
本例中,在项目工程下的 build.gradle 声明,修改后如下:

6.1 修改 com.android.tools.build:gradle 版本

dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        ...
    }

6.2 原 compile 指令换成 api

6.3 原 testCompile 指令换成 testImplementation

6.4 原 provided 指令换成 compileOnly

6.5 原 instrumentTest 换成 androidTest

6.6 在 gradle.properties 文件增加两个属性

android.enableD8.desugaring=true
android.useDexArchive=true

当然,还可能有其他需要适配的地方,这就跟项目有关了,改到这个地方,我的问题已经基本解决了。

七、最后的分析

按照上述的解决问题分析,最终顺利生成了报告。
总结了下,其实在这个过程中,我们需要做的地方,最重要的也就是在 debug 版本里打开 testCoverageEnabled 这个开关,剩余的事情,其实根生成报告的方式有关了,如果按照 api 来生成报告,理论上来说,关于 jacoco 的其他地方,都不需要改动。

7.1 gradle 的这个 jacoco plugin

肯定是要 apply 这个插件的,这是前提

7.2 testCoverageEnabled

这个属性,在 debug 版本里,要设置为 true,不然会无法生成

7.3 jacoco 的 toolVersion

这个版本号的设置,其实只影响用 task 来生成 jacoco 的报告,以及在编译期运行单元测试的时候生效,在你打完 debug 包进入 apk 之后,其实它就不生效了

7.4 关于对 jacoco 的配置

在 build.gradle 中对 jacoco 的配置,其实只是针对 jacoco 做了任务的扩展,可以让你改变 jacoco 插件对 jacoco 库引用的一些默认配置

比如生成报告的时候引用的 jacoco agent 和 jacoco ant
以及扩展一些其他生成报告的任务,可以修改 class 的文件夹属性和 src 属性以及 exec 文件的路径

(这里理解的比较浅,可能会有误解,还请大家仔细研究)

7.5 apk 运行期间生成 exec 数据

划个重点:
因为我们是对 apk 的运行期做的覆盖率数据统计,所以主管这个 jacoco 数据生成的,其实控制在下面这个库里

classpath 'com.android.tools.build:gradle:3.2.1'

这么看来,社区的文章里面介绍时,如此的顺利,也恰好是因为他们用的这个构建版本刚好是支持 0.7.5 以上的 jacoco core,这好像在 3.1.4(获取还有再低点的,我没仔细看) 以后就支持了,我选择的 3.2.1 内置的好像是 0.7.9+ 的

八、最后贴一下我们用的覆盖率平台 (其实不能算是平台,只能说是内部用的一个小工具,太丑了)

整体界面如下:

覆盖率统计列表界面:

覆盖率信息配置界面:

太丑了,不贴了,也因为这个平台踩了很多坑。。
有人会问,做这个平台的意义是什么,jacoco 已经做的那么完善了?
前面也说过了,主要是可以节约其他测试人员的一些学习成本,毕竟不是每个人都愿意投入时间去学这么多杂的东西,另外也是个人对开发的爱好~~~

最后,我太啰嗦了,写了这么长,祝大家生活顺利、工作顺利~~~

共收到 21 条回复 时间 点赞

溜得一笔👍

666
我把 Jacoco 代码拉下来,eclipse idea 都试过,编译总是报错,蛋疼了几周了,编译都没搞定,感觉整个人都不好了

onesbyones 回复

哈哈,瞎胡混😃

hello 回复

这个报错要看是什么类型的,有的只是飘红其实不影响大局,我这里也有飘红的,不过没关系。

还有就是编译打包的时候最好去掉 javadoc 和 test 这两像这样

mvn clean package -Dmaven.javadoc.test=true -Dmaven.test.skip=true

他就会跳过生成文档和单元测试阶段,因为 jacoco 里面本身有这些东西,但有些代码比较老,可能会报警或者报错。
我现在自己打包就这样

好的,多谢,我再试试。我的报错打包不成功

[INFO] JaCoCo :: Test :: Agent ............................ SUCCESS [ 3.429 s]
[INFO] JaCoCo :: Test :: Ant .............................. SUCCESS [01:17 min]
[INFO] JaCoCo :: Test :: Maven Plugin ..................... SUCCESS [ 2.567 s]
[INFO] JaCoCo :: Examples ................................. SUCCESS [ 11.234 s]
[INFO] JaCoCo :: Test :: Examples ......................... SUCCESS [ 2.782 s]
[INFO] JaCoCo :: Documentation ............................ FAILURE [ 6.912 s]
[INFO] JaCoCo :: Distribution ............................. SKIPPED
[INFO] root ............................................... SKIPPED
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 05:09 min
[INFO] Finished at: 2019-09-20T12:30:05+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.jacoco:jacoco-maven-plugin:0.7.9:report-aggregate (report-aggregate) on project org.jacoco.doc: Execution report-aggregate of goal org.jacoco:jacoco-maven-plugin:0.7.9:report-aggregate fai
led.: NullPointerException -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException
[ERROR]
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR] mvn -rf :org.jacoco.doc

方便加个 Q 么 512433465

hello 回复


我看你用的 jacoco0.7.9,临时拉了一个下来,试了下,OK 的呀

匿名 #9 · September 20, 2019

楼主问一下,现在很多 app 都是混合应用了,或者干脆 h5 套个壳,对这种应用,可能有原生的逻辑也可能有 web 的逻辑,要怎么去做覆盖率呢

这个有点专业,我也不太能回答上来,我请教了下思寒,看他有没有时间帮我们分析吧。

按照我的理解,你说的这种场景,首先 java 的和 kotlin 的本身没有问题,按照原来的思路就行;
涉及到 h5 的,我之前分析过 pc 端的,除了 js 的分析起来还有点意义,CSS 和 HTML 的分析起来意义并不大,而且 js 有专门的覆盖率工具,社区里有分析过 React native 的,你可以看看对你有没有帮助。

这点我也是了解的不多

匿名 #11 · September 20, 2019

如果是这样 等于 web 和原生其实是分开的 其实对覆盖率而言 不止是前端(客户端 ui+web ui)的覆盖率有意义,后端的覆盖率也有意义甚至占比更大,如果能有一个全方位的覆盖率统计方式,以统一展示提供服务,应该收益会更大

道理肯定是这么个道理,但是不同的语言群体有不同的工作机制,目前来看好像还没有能统一的一种方式或者工具。
一个项目下来,那肯定是不能拆开来算的,拿到前端、app、服务端的数据,应该是要想办法统一梳理的,隔离开来单独展示的意义其实不是很大,那就看以什么样的配比来统计了吧。

我之前研究过前端的一些方式,最后搁置了~~目前来看,app 端和后端的稍微好弄一些。

前几天跟思寒请教了下,大概总了以下几点,特来回复你,希望对你有所帮助:

  1. 既然你也说了,是混合应用,那我就理解为有前端和原声揉在一起,那么技术涉及肯定是有前端和原生相关的,你也应该清楚,前端和原生分别有自己的覆盖率收集技术
  2. 既然如此,那可以各自用各自的技术,在测试 APP 的时候,同时启动前端和原生应用的覆盖率收集,但是各自产生各自的覆盖率报告
  3. 如果需要的话,两份数据做个整合,统一算一份 App 的覆盖率报告,至于怎么整合,比例怎么算,是单纯按照行数或者方法数求加权,还是其他算法,这个自己说了算。

举个栗子:
前端的用 istanbul 来集成进去,在 app 运行的某个阶段开启前端覆盖率的收集,
原生那边就用 jacoco,找到一个合适的契机,比如跟现在一样在退出 app 的时候,去分别生成自己的数据 (至于代码怎么写要研究下了),然后取到覆盖率数据,就可以分别生成报告了。

当然可能我描述的并不是很准确,在转述时也有可能转达丢了意思,要是是在有需求可以帖子里好好研究下,或者寻求帮助,我对前端的覆盖率这块不是很熟。
祝好运哈!😺

Istanbul 做这个思路是对的,不过对于 react 这样的小程序应用,直接在 app 内做不行,必须现在 react 编译阶段之前插桩,然后编译,数据上传后再和编译前的 react 应用对应。

大佬,AngryTester jacoco 执行 mvn install -Dmaven.test.skip=true -pl jacoco-maven-plugin -am 报错。报错如下

leilei 回复
mvn install -Dmaven.test.skip=true -pl jacoco-maven-plugin -am
java
mvn install -Dmaven.test.skip=true -pl org.jacoco.agent.rt -am

这两个命令交替执行下再试试

针对一个项目跑了增量覆盖率 ,为啥都是 0,sessions 这一页是啥含义

我收集到的覆盖率也是空的数据

对于 android sdk 项目的测试覆盖率怎么统计呢,求思路

方便加个 QQ 吗,1509783568,有问题想请教大佬,麻烦加一下,感谢。👃

请教一个问题:类里 4 个方法如果有一个修改了,代码重新部署后 整个类覆盖率是 0,有没有解决办法?毕竟这种情况很常见的,怎么满足?谢谢

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up