阅读本文大约需要2.1分钟。
点击?小卡片,回复 “合集” 获取系统性的学习笔记和测试开发技能图谱
前言
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版本比较新,应该算是目前主流的项目开发环境了:
代码语言:javascript复制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文件,具体代码如下所示:
代码语言:javascript复制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,如下所示:
代码语言:javascript复制apply from: 'jacoco.gradle'
...do something
android {
...
}
我们再整理一个jacoco.gradle放在项目的根目录作为通用配置,内容如下:
代码语言:javascript复制apply plugin: 'jacoco'
android {
buildTypes {
debug {
/**打开覆盖率统计开关**/
testCoverageEnabled = true
}
}
}
如果需要统计子module中的代码覆盖率,那么需要在子module的build.gradle文件中添加如下依赖:
代码语言:javascript复制apply from: rootProject.file('jacoco.gradle')
第二步
定义一个JacocoHelper类,主要是用来生成ec文件,根据使用场景可以放在你需要的地方,比如在APP内提供一个按钮,点击触发生成ec文件,也可以通过命令行的方式触发,具体代码如下:
代码语言:javascript复制package com.android.jarvis.jacoco;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class JacocoHelper {
private static final String TAG = "JacocoHelper";
//ec文件的路径
private static final String DEFAULT_COVERAGE_FILE_PATH = Environment.getExternalStorageDirectory()
.getPath() "/coverage.ec";
/**
* 生成ec文件
*
* @param isNew 是否重新创建ec文件
*/
public static void generateEcFile(boolean isNew) {
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
try {
if (isNew && mCoverageFilePath.exists()) {
Log.d(TAG, "清除旧的ec文件");
mCoverageFilePath.delete();
}
if (!mCoverageFilePath.exists()) {
mCoverageFilePath.createNewFile();
}
out = new FileOutputStream(mCoverageFilePath.getPath(), true);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
if (agent != null) {
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
}
} catch (Exception e) {
Log.d(TAG, e.toString());
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在需要生成ec文件的地方调用下面的方法:
代码语言:javascript复制JacocoHelper.generateEcFile(true);
生成测试报告
通过上面的两个步骤,我们就完成了Android项目的Jacoco配置,下面再教大家如何使用它来获取我们手工或者自动化测试的代码覆盖率。
首先我们可以通过Android Studio直接编译安装集成了Jacoco的Debug包,然后再在项目的根目录执行下面的命令完成初始化:
代码语言:javascript复制./gradlew jacocoInit
接着我们就可以通过执行自动化测试脚本或者手工来开始我们的用例测试了,测试完成后执行下面的命令:
代码语言:javascript复制adb pull /storage/emulated/0/coverage.ec .
把得到的coverage.ec文件放到下图所示的位置,其中code-coverage目录就是执行初始化脚本生成的。
最后我们在项目根目录执行下面的命令来生成报告:
代码语言:javascript复制./gradlew jacocoTestReport
在下图所示位置,我们就可以看到覆盖率的报告了
注意:
上面的命令中用的是./gradlew,但也可以替换为gradle,两者的区别这里跟大家稍微解释一下,gradlew其实就是对gradle的包装和配置,gradlew是gradle Wrapper,Wrapper的意思就是包装。
因为不是每个人的电脑中都安装了gradle,也不一定安装的版本就是要编译项目需要的版本,那么gradlew里面就配置了项目需要的gradle版本,用户只需要运行gradlew就可以按照配置下载对应的gradle到项目的目录中,仅仅给项目本身用。
报告分析
生成的报告如下:
点击包名你可以看到类的覆盖率情况
再点击类名,可以看到具体哪些代码被调用到了,又有哪些代码没有被调用到
落地场景
我们既然可以根据执行的用例拿到代码的覆盖情况,那么我们就可以基于这个数据来做度量和精准测试了,比如:
1、可以让业务QA或者外包使用覆盖率包来完成功能模块的测试工作,这样就可以根据生成的覆盖率数据来度量测试效果了;
2、另外在做精准测试的时候,我们都需要维护用例和代码的关系库,那么如何得到这个关系呢,这时候我们就可以通过在手工或者UI自动化的方式执行用例的过程中把每个用例跟对应覆盖的代码类文件建立映射关系来完成初期的关系库;