前言 :jacoco是Java Code Coverage的缩写,是Java代码覆盖率统计的主流工具之一。关于jacoco的原理介绍的文章在网上有很多,感兴趣的同学可以去找别的博客看看,我这里不做赘述。它的作用是在安卓项目的代码覆盖率统计使用了jacoco的离线插桩方式,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试(单元测试、UI测试或者手工测试等)插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
一、环境确认(可以改用自己实际的环境)
1. 根目录build.gradle插件版本
classpath 'com.android.tools.build:gradle:4.0.1'
2.gradle依赖版本(gradle-wrapper.properties):
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
3.app模块build.gradle的android sdk版本:
minSdkVersion 27
targetSdkVersion 30
二、开始实践
1.创建一个jacoco.gradle文件:
是提供给app模块build.gradle使用的,负责依赖jacoco插件,指定jacoco版本号,并且创建一个生成报告的任务,具体代码如下所示:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.2"
}
//源代码路径,你有多少个module,你就在这写多少个路径
def coverageSourceDirs = [
'/src/main/java'
]
//class文件路径,就是我上面提到的class路径,看你的工程class生成路径是什么,替换一下就行
def coverageClassDirs = [
'build/intermediates/javac/debug/classes'
]
//这个就是具体解析ec文件的任务,会根据我们指定的class路径、源码路径、ec路径进行解析输出
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories.setFrom(fileTree(
dir: 'build/intermediates/javac/debug/classes',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
]))
sourceDirectories.setFrom(files(coverageSourceDirs))
executionData.setFrom(files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"))
doFirst {
//遍历class路径下的所有文件,替换字符
coverageClassDirs.each { path ->
new File(path).eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
}
2.在build.gradle文件中添加依赖
依赖上面的jacoco.gradle,下面是一个通用的示例:
apply from: 'jacoco.gradle'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 27
targetSdkVersion 30
multiDexEnabled true
...
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
testCoverageEnabled = true
debuggable true
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
3.配置AndroidManifest.xml
在app模块的AndroidManifest.xml添加一些配置,配置我们上面添加的InstrumentedActivity和JacocoInstrumentation。
//添加所需的权限
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
...
android:largeHeap="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<activity
android:name=".test.InstrumentedActivity"
android:label="InstrumentationActivity" />
</application>
<instrumentation
android:name=".test.JacocoInstrumentation"
android:handleProfiling="true"
android:label="CoverageInstrumentation"
android:targetPackage="com.example.myapplication" />
配置完后,会发现targetPackage="com.example.myapplication"显红,但是没关系,不用管它,targetPackage对应的是应用的包名。
4.在com.example.myapplication下创建一个test包
以下列出的这几个文件和网上其他的一样,可以直接拿过来用,这里其实用的是监听我们的主Activity,这个一般是咱们app的首页MainActivity,不要把它理解为启动Activity,做的就是一件事,当这个Activity执行onDestroy方法的时候通知Instrumentation生成ec文件,所以你不想根据这种思路来走完全没有问题,实现一个工具类,在你想要执行生成ec文件的时候调用即可,道理一样,看你的使用场景和需求。
1、FinishListener
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
2、InstrumentedActivity
public class InstrumentedActivity extends MainActivity {
public FinishListener finishListener;
public void setFinishListener(FinishListener finishListener) {
this.finishListener = finishListener;
}
@Override
public void onDestroy() {
if (this.finishListener != null) {
finishListener.onActivityFinished();
}
super.onDestroy();
}
}
3、JacocoInstrumentation
public class JacocoInstrumentation extends Instrumentation implements FinishListener {
public static String TAG = "JacocoInstrumentation:";
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
private final Bundle mResults = new Bundle();
private Intent mIntent;
private static final boolean LOGD = true;
private boolean mCoverage = true;
private String mCoverageFilePath;
public JacocoInstrumentation() {
}
@Override
public void onCreate(Bundle arguments) {
LogUtil.e(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (file.isFile() && file.exists()) {
if (file.delete()) {
LogUtil.e(TAG, "file del successs");
} else {
LogUtil.e(TAG, "file del fail !");
}
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
LogUtil.e(TAG, "异常 : " + e);
e.printStackTrace();
}
}
if (arguments != null) {
LogUtil.e(TAG, "arguments不为空 : " + arguments);
mCoverageFilePath = arguments.getString("coverageFile");
LogUtil.e(TAG, "mCoverageFilePath = " + mCoverageFilePath);
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}
@Override
public void onStart() {
LogUtil.e(TAG, "onStart def");
if (LOGD) {
LogUtil.e(TAG, "onStart()");
}
super.onStart();
Looper.prepare();
InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
activity.setFinishListener(this);
}
private boolean getBooleanArgument(Bundle arguments, String tag) {
String tagString = arguments.getString(tag);
return tagString != null && Boolean.parseBoolean(tagString);
}
private void generateCoverageReport() {
OutputStream out = null;
try {
out = new FileOutputStream(getCoverageFilePath(), false);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
} catch (Exception e) {
LogUtil.e(TAG, e.toString());
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private String getCoverageFilePath() {
if (mCoverageFilePath == null) {
return DEFAULT_COVERAGE_FILE_PATH;
} else {
return mCoverageFilePath;
}
}
private boolean setCoverageFilePath(String filePath) {
if (filePath != null && filePath.length() > 0) {
mCoverageFilePath = filePath;
return true;
}
return false;
}
private void reportEmmaError(Exception e) {
reportEmmaError("", e);
}
private void reportEmmaError(String hint, Exception e) {
String msg = "Failed to generate emma coverage. " + hint;
LogUtil.e(TAG, msg);
mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
+ msg);
}
@Override
public void onActivityFinished() {
if (LOGD) {
LogUtil.e(TAG, "onActivityFinished()");
}
if (mCoverage) {
LogUtil.e(TAG, "onActivityFinished mCoverage true");
generateCoverageReport();
}
finish(Activity.RESULT_OK, mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath) {
// TODO Auto-generated method stub
if (LOGD) {
LogUtil.e(TAG, "Intermidate Dump Called with file name :" + filePath);
}
if (mCoverage) {
if (!setCoverageFilePath(filePath)) {
if (LOGD) {
LogUtil.e(TAG, "Unable to set the given file path:" + filePath + " as dump target.");
}
}
generateCoverageReport();
setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
}
}
}
5.生成测试报告
1、installDebug
首先我们通过命令行安装app,选择你的app -> Tasks -> install -> installDebug,安装app到你的设备上。
2、命令行启动app
adb shell am instrument com.example.myapplication/com.example.myapplication.test.JacocoInstrumentation
3、正常点击测试app的功能
这个时候你可以操作你的app,对你想进行代码覆盖率检测的地方,进入到对应的页面,点击对应的按钮,触发对应的逻辑,你现在所操作的都会被记录下来,在生成的coverage.ec文件中都能体现出来。当你点击完了,根据我们之前设置的逻辑,当我们MainActivity执行onDestroy方法时才会通知JacocoInstrumentation生成coverage.ec文件,我们可以按返回键退出MainActivity返回桌面,生成coverage.ec文件可能需要一点时间哦(取决于你点击测试页面多少,测试越多,生成文件越大,所需时间可能多一点)
然后在Android Studio的Device File Explore中,找到data/data/包名/files/coverage.ec文件,右键保存到桌面(随你喜欢)备用。
4、createDebugCoverageReport
点击Android Studio的右上角“Gradle”,这个命令正常存在的路径是
应用名称/app/verification/createDebugCoverageReport
双击它,会执行创建覆盖率报告的命令,等待它执行完,这个会在…/app/build/outputs/code_coverage/debugAndroidTest/connected下生成一个covera.ec文件,但是这个不是我们最终需要分析的,我们需要分析的是我们刚才手动点击保存到桌面的那个。然后把桌面的那个coverage.ec文件拷贝到这个路径下(当然coverage.ec文件拷贝到哪个路径都可以改,你的jacoco.gradle中执行的executionData对应的路径也得配套修改)
5、jacocoTestReport
点击Android Studio的右上角“Gradle”,路径是
应用名称/app/reporting/jacocoTestReport
找到这个路径,双击执行这个任务,会生成我们最终所需要代码覆盖率报告,执行完后,我们可以在这个目录下找到它
应用名称\app\build\reports\jacoco\jacocoTestReport\coverageTestReport\index.html
在文件夹下双击打开就能看到我们的代码覆盖率报告。