背景
之前做了代码覆盖率工具的调研,目前基于现有条件尝试可落地的方案
JaCoCo工具介绍
JaCoCo是面向Java的开源代码覆盖率工具,JaCoCo以Java代理模式运行,它负责在运行测试时检测字节码。 JaCoCo会深入研究每个指令,并显示每个测试过程中要执行的行。 为了收集覆盖率数据,JaCoCo使用ASM库即时进行代码检测,并在此过程中从JVM Tool Interface(Java虚拟机提供的一种原生编程接口)接收事件,最终生成代码覆盖率报告。
官网地址:https://www.jacoco.org/jacoco/index.html
运行模式
JaCoCo有离线(offline)、在线(on the fly)运行模式
离线模式(offline):
- 使用方式:离线模式需要在编译时对代码进行插桩,即在测试前对文件进行插桩,生成插过桩的class或jar包。测试完成后,再对这些插桩的class和jar包进行分析,生成覆盖报告。
- 应用场景:离线模式适用于特定的场景,如当测试环境无法直接运行带有Jacoco agent的代码时,或者需要在没有网络连接的环境中进行测试时使用。然而,离线模式存在一些缺点,如必须恢复原始类,这增加了操作的复杂性。
在线模式(on the fly):
- 使用方式:在线模式在应用启动时加入Jacoco agent进行插桩,允许在开发、测试人员使用应用期间实时地进行代码覆盖率分析。这种方式更加便捷,能够实时反映代码覆盖率的变化。
- 应用场景:在线模式是进行代码覆盖率分析的首选方法,因为它能够提供实时的反馈,帮助开发人员及时调整代码以满足覆盖率要求。在线模式适用于大多数常规的软件开发和测试环境。
相信很多的java项目开发人员并不会去写单元测试代码的,因此覆盖率统计将手工测试或接口测试覆盖的情况作为重要依据,显然在线模式更符合实际需求,它使得即使不编写单元测试用例,也能通过手工测试生成覆盖率报告。
在线模式的操作流程大致分为三个步骤:
- 启动应用增加Jacoco agent进行插桩:这一步是在应用启动时,通过添加Jacoco agent来实现代码的插桩。这个过程中,Jacoco agent会启动一个TCP Server,以便后续收集覆盖率数据。
- 从TCP Server dump生成代码覆盖率文件:在应用运行过程中,Jacoco agent会收集代码覆盖率数据,并将这些数据以.exec格式的二进制文件形式保存下来。这个文件包含了所有收集到的覆盖率信息。
- 解析.exec格式文件生成html格式代码覆盖率报告:最后,通过解析.exec格式的文件,可以生成html格式的代码覆盖率报告。这个报告会详细展示哪些代码被执行了,哪些代码没有被执行,以及具体的覆盖率数据,从而帮助开发人员了解代码的质量和测试的充分性。
通过Jacoco在线模式,开发人员可以在开发过程中实时了解代码的覆盖率情况,这对于提高软件质量、优化测试策略以及确保代码的全面测试非常有帮助
部署步骤
环境说明
开发工具:AndroidStudio
Gradle版本:distributionUrl=https://services.gradle.org/distributions/gradle-8.7-bin.zip
Gradle插件版本:com.android.tools.build:gradle:8.0.2
步骤1:
在app模块下新建一个jacoco.gradle文件
具体代码如下所示:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.8"
}
android {
buildTypes {
debug {
/**打开覆盖率统计开关**/
testCoverageEnabled = true
}
}
}
//这个就是具体解析ec文件的任务,会根据我们指定的class路径、源码路径、ec路径进行解析输出
tasks.register('jacocoTestReport', JacocoReport) {
reports {
xml.required = true
html.required = true
}
// 源代码路径,有多少个module,就在这里写多少个路径,如果你只有app一个module,那么就写一个就可以
def SourceDirs = [
'../app/src/main/java'
]
sourceDirectories = files(SourceDirs)
classDirectories = fileTree(
//class文件路径(以项目 class 文件所在目录为准),如果你只有app一个module,那么就写一个就可以
dir: "$buildDir/intermediates/javac/debug/compileDebugJavaWithJavac/classes",
// 过滤不需要统计的class文件
excludes: ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*']
)
executionData = files(
"$buildDir/outputs/code_coverage/coverage.ec"
)
}
//初始化Jacoco Task
tasks.register('jacocoInit') {
group = "JacocoReport"
doFirst {
File file = new File("$buildDir/outputs/code_coverage/")
if (!file.exists()) {
file.mkdir();
}
}
}
其中class的文件路径,具体跟gradle的版本有关,需要查看你自己实际的路径,如下图:
步骤2:
然后在你的app模块下的build.gradle文件中依赖这个jacoco.gradle,如下所示:
步骤3:
我们再整理一个jacoco.gradle放在项目的根目录作为通用配置,位置如下:
代码如下:
apply plugin: 'jacoco'
android {
buildTypes {
debug {
/**打开覆盖率统计开关**/
testCoverageEnabled = true
}
}
}
如果需要统计子module中的代码覆盖率,那么需要在子module的build.gradle文件中添加如下依赖:
apply from: rootProject.file('jacoco.gradle')
步骤4:
定义一个JacocoHelper类,主要是用来生成ec文件,根据使用场景可以放在你需要的地方,比如在APP内提供一个按钮,点击触发生成ec文件,也可以通过命令行的方式触发,具体代码如下:
package com.example.myapplication.ui;
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";
// private static final String DEFAULT_COVERAGE_FILE_PATH = "/sdcard/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()) {
Log.d(TAG, "新建ec文件");
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();
}
}
}
}
步骤5:
在需要生成ec文件的地方调用下面的方法:
JacocoHelper.generateEcFile(true);
步骤6(生成测试报告):
通过上面的两个步骤,我们就完成了Android项目的Jacoco配置,下面是如何使用它来获取我们手工或者自动化测试的代码覆盖率。
首先我们可以通过Android Studio直接编译安装集成了Jacoco的Debug包,然后再在项目的根目录执行下面的命令完成初始化:
./gradlew jacocoInit
接着我们就可以通过执行自动化测试脚本或者手工来开始我们的用例测试了,测试完成后执行下面的命令:
adb pull /storage/emulated/0/coverage.ec ./app/build/outputs/code_coverage/
把得到的coverage.ec文件放到下图所示的位置,其中code_coverage目录就是执行初始化脚本生成的。
最后我们在项目根目录执行下面的命令来生成报告:
./gradlew jacocoTestReport
步骤7(查看报告):
在下图所示位置,我们就可以看到覆盖率的报告了:
用浏览器打开后可以查看报告如下:
红色进度条表未覆盖,绿色进度条表示已覆盖,Cov 为总体覆盖率
点击包名,你可以看到类的覆盖率情况:
再点击类名,你可以看到方法的覆盖率情况:
点击方法名后看到对应类中的代码执行情况,如下图:
覆盖率指标说明
JaCoCo从多种角度对代码进行了分析,包括指令(Instructions,C0 Coverage),分支(Branches,C1 Coverage),圈复杂度(Cyclomatic Complexity),行(Lines),方法(Methods),类(Classes)。
1、Instructions
Jacoco计算的最小单位就是字节码指令。指令覆盖率表明了在所有的指令中,哪些被指令过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息。
2、Branches
Jacoco对所有的if和switch指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内。 在有调试信息的情况下,分支点可以被映射到源码中的每一行,并且被高亮表示。
红色钻石:无覆盖,没有分支被执行。
黄色钻石:部分覆盖,部分分支被执行。
绿色钻石:全覆盖,所有分支被执行。
3、Cyclomatic Complexity
Jacoco为每个非抽象方法计算圈复杂度,并也会计算每个类,包,组的复杂度。根据McCabe1996的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效。
4、Lines
该项指数在有调试信息的情况下计算。因为每一行代码可能会产生若干条字节码指令,所以我们用三种不同状态表示行覆盖率
红色背景:无覆盖,该行的所有指令均无执行。
黄色背景:部分覆盖,该行部分指令被执行。
绿色背景:全覆盖,该行所有指令被执行。
5、Methods
每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为JaCoCo直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。
6、Classes
每个类中只要有一个方法被执行,这个类就被认定为被执行。同5一样,有些没有在源码声明的方法被执行,也认定该类被执行。
参考文档
https://www.cnblogs.com/wiggins/p/16065992.html
https://blog.youkuaiyun.com/Dream_Weave/article/details/119808755
https://blog.youkuaiyun.com/Dream_Weave/article/details/119808755