前言
JaCoCo的概念我就不在这里复述了网上有很多资料介绍,这里主要提一下他的两种插桩模式:On-the-fly和Offline
On-the-fly模式:
JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否需要转换修改class文件,然后将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。
Offline模式:
在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
但在Android项目中只能使用JaCoCo的离线插桩模式,主要是因为Android系统破坏了JaCoCo的这种便利性,原因如下:
- Android虚拟机跟运行在服务器上的JVM不同,它所支持的字节码必须经过特殊的处理以支持Dalvik、ART等虚拟机,所以插桩必须在处理之前完成;
- Android虚拟机无法像服务器上的JVM那样可以通过参数的方式实现配置,所以应用启动的时候是没有机会直接配置dump输出方式获取覆盖率信息的;
背景
其实主要是基于两个痛点:
1、新功能测试和回归测试在手工测试的情况下,即便用例写的再怎么详细,也经常会有漏测的发生,这里一方面是因为现在大量互联网公司采用外包资源来做业务测试,而外包的工作质量无法有效评估,可能存在漏执行的情况,另外一方面是本身测试用例设计的不够完善导致没有覆盖到一些关键路径的代码分支,因此亟需一种可以度量手工测试完成后对代码覆盖情况的手段或者工具;
2、研发代码变更的影响范围难以精准评估,比如研发提交一个MR,这个MR到底影响了多少用例,在没有精准测试能力的情况下是很难给出的,而做精准测试,最重要的一环就是代码用例的关系库维护,如何生成代码跟用例的关系,就需要用到代码覆盖率的采集和分析能力了;
实战
其实基于jacoco来做Android端代码覆盖率的难点主要是各个项目的gradle插件依赖跟jacoco版本直接的兼容性问题,特别是在以及开发很多年的多模块项目下,这个问题尤为明显,另外网上虽然有很多相关的文章资料,但是要么是gradle插件依赖版本太低,要么就是jacoco版本、配置文件以及项目的开发环境没有说清楚或者写的有问题,导致最终很难按照说明完成接入。
因此我先说明一下我的依赖情况,我用的是4.0版本比较新,应该算是目前主流的项目开发环境了:
gradle插件版本:classpath 'com.android.tools.build:gradle:4.0.1' gradle依赖版本:distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
我这里直接以多模块项目为例,单模块项目修改jacoco.gradle配置文件中的源码路径和class文件路径即可。
第一步
在app模块下新建一个jacoco.gradle文件,具体代码如下所示:
apply plugin: 'jacoco' android { buildTypes { debug { /**打开覆盖率统计开关**/ testCoverageEnabled = true } } } //源代码路径,有多少个module,就在这里写多少个路径,如果你只有app一个module,那么就写一个就可以 def coverageSourceDirs = [ '../app/src/main/java', '../common/src/main/java', ] //class文件路径,如果你只有app一个module,那么就写一个就可以 def coverageClassDirs = [ '/app/build/intermediates/javac/debug/classes', '/common/build/intermediates/javac/debug/classes', ] //Jacoco 版本,建议用这个版本兼容性比较好 jacoco { toolVersion = "0.8.2" } //生成报告task task jacocoTestReport(type: JacocoReport) { group = "JacocoReport" description = "Generate Jacoco coverage reports after running tests." reports { xml.enabled = true html.enabled = true } classDirectories.from = files(files(coverageClassDirs).files.collect { println("$rootDir" + it) fileTree(dir: "$rootDir" + it, // 过滤不需要统计的class文件 excludes: ['**/R*.class', '**/*$InjectAdapter.class', '**/*$ModuleAdapter.class', '**/*$ViewInjector*.class' ]) }) sourceDirectories.from = files(coverageSourceDirs) executionData.from = files("$buildDir/outputs/code-coverage/coverage.ec") doFirst { coverageClassDirs.each { path -> println("$rootDir" + path) new File("$rootDir" + path).eachFileRecurse { file -> if (file.name.contains('$$')) { file.renameTo(file.path.replace('$$', '$')) } } } } } //初始化Jacoco Task task jacocoInit() { group = "JacocoReport" doFirst { File file = new File("$buildDir/outputs/code-coverage/") if (!file.exists()) { file.mkdir(); } } }
其中class的文件路径,具体跟gradle的版本有关,需要查看你自己实际的路径,如下图:
然后在你的app模块下的build.gradle文件中依赖这个jacoco.gradle,如下所示:
apply from: 'jacoco.gradle'...do something android {...}
我们再整理一个jacoco.gradle放在项目的根目录作为通用配置,内容如下:
apply plugin: 'jacoco' android { buildTypes { debug { /**打开覆盖率统计开关**/ testCoverageEnabled = true } } }
如果需要统计子module中的代码覆盖率,那么需要在子module的build.gradle文件中添加如下依赖:
apply from: rootProject.file('jacoco.gradle')