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

zailushang · October 09, 2018 · Last by zailushang replied at October 15, 2020 · Last modified by admin 恒温 · 13601 hits
本帖已被设为精华帖!

示例JacocoTestForAndroid见:https://github.com/OnTheWay111/JacocoTestForAndroid
还有一个更灵活的非instrument方式: [定制触发条件] jacoco 统计 Android 代码覆盖率
本文两年前的代码,有些代码可能已过时,如果遇到问题,可参考:https://www.jianshu.com/p/ab2be01d7347#comment-60999311

1、背景

最近团队想基于monkey,进行app遍历开发,为了与市场上的遍历方案做对比,以及团队遍历方案的优化比较,故选择了APP的代码覆盖率统计。

备注:Instrumentation方式1 和 Instrumentation方式2 几乎一样

2、代码覆盖率方案选型

目前Java常用覆盖率工具Jacoco、Emma、Cobertura和Clover(商用)

Jacoco 是一个开源的覆盖率工具。Jacoco 可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技术监控 Java 程序。很多第三方的工具提供了对 Jacoco 的集成,如:Sonar、Jenkins、IDEA.
综合考虑:选择jacoco作为代码覆盖率统计工具。

3、jacoco注入原理


本次,在尽量避免修改源代码的情况下,我们选择instrumentation模式,开始操作。

4、jacoco注入实操(instrument方式)

4.1 创建一个安卓项目

随便创建一个简单的安卓项目

4.2 在代码主路径下新建test文件夹,新建3个类文件

在java/下新建test文件夹,然后在test路径下
新建三个类文件,分别是抽象类FinishListener,Instrumentation的Activity和instrumentation类。

  • FinishListener:
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
  • InstrumentedActivity:
public class InstrumentedActivity extends MainActivity {
public static String TAG = "IntrumentedActivity";
private FinishListener mListener;
public void setFinishListener(FinishListener listener) {
mListener = listener;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG + ".InstrumentedActivity", "onDestroy()");
super.finish();
if (mListener != null) {
mListener.onActivityFinished();
}
}
}
  • 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) {
Log.d(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath().toString() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if(!file.exists()){
try{
file.createNewFile();
}catch (IOException e){
Log.d(TAG,"新建文件异常:"+e);
e.printStackTrace();}
}
if(arguments != null) {
mCoverageFilePath = arguments.getString("coverageFile");
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}

@Override
public void onStart() {
super.onStart();
Looper.prepare();
InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
activity.setFinishListener(this);
}

private String getCoverageFilePath() {
if (mCoverageFilePath == null) {
return DEFAULT_COVERAGE_FILE_PATH;
} else {
return mCoverageFilePath;
}
}

private void generateCoverageReport() {
Log.d(TAG, "generateCoverageReport():" + getCoverageFilePath());
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) {
Log.d(TAG, e.toString(), e);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

@Override
public void onActivityFinished() {
if (LOGD) Log.d(TAG, "onActivityFinished()");
if (mCoverage) {
generateCoverageReport();
}
finish(Activity.RESULT_OK, mResults);
}

@Override
public void dumpIntermediateCoverage(String filePath) {

}
}

4.3 修改Build.gradle文件

增加jacoco插件和打开覆盖率统计开关

apply plugin: 'com.android.application'
apply plugin: 'jacoco'

android {
compileSdkVersion 28
buildToolsVersion "28.0.2"
defaultConfig {
applicationId "com.example.app1"
minSdkVersion 24
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
/**打开覆盖率统计开关*/
testCoverageEnabled = true
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

4.4 在manifest.xml增加声明

  • 在中声明InstrumentedActivity:
<activity android:label="InstrumentationActivity"    android:name="coverage.netease.com.test.InstrumentedActivity" />
  • 在manifest中声明使用SD卡权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  • 最后,在manifest中单独声明JacocoInstrumentation:
<instrumentation
android:handleProfiling="true"
android:label="CoverageInstrumentation"
/*这里android:name写明此instrumentation的全称*/
android:name="com.example.test.JacocoInstrumentation"
/*这里android:targetPackage写明被测应用的包名*/
android:targetPackage="com.example.app1"/>

4.5 通过gradle installDebug安装到设备上

4.6 在命令行下通过adb shell am instrument命令调起app,具体命令是:

adb shell am instrument com.example.app1/com.example.test.JacocoInstrumentation

也就是adb shell am instrument /

4.7 调起app后我们就可以进行手工测试了,测试完成后点击返回键退出app。

此时,jacoco便将覆盖率统计信息写入/data/data//files/coverage.ec文件。
接下来我们需要新增gradle task,分析覆盖率文件生成覆盖率html报告。

5、生成html报告

5.1 首先将coverage.ec文件拉到本地,置于指定目录下。

  • adb shell进入设备,找到data/data//files/coverage.ec文件。
  • adb pull到pc本地。
  • 将该文件拖至入app根目录/build/outputs/code-coverage/connected(没有的话,可以执行gradle createDebugCoverageReport)

5.2 新增gradle task,修改build.gradle文件为:

apply plugin: 'com.android.application'
apply plugin: 'jacoco'

android {
compileSdkVersion 28
buildToolsVersion "28.0.2"
defaultConfig {
applicationId "com.example.app1"
minSdkVersion 24
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
/**打开覆盖率统计开关*/
testCoverageEnabled = true
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

def coverageSourceDirs = [
'../app/src/main/java'
]

task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: './build/intermediates/app_classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/
*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")

doFirst {
new File("$buildDir/intermediates/app_classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}

5.3 生成html报告

  • 执行:
gradle jacocoTestReport

执行完以上命令后,html报告生成

  • 查看html报告 报告路径://app/build/reports/jacoco/jacocoTestReport/html

打开报告后,如图所示:

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 72 条回复 时间 点赞

需要注意的是:build.gradle文件中,app_classes的名字需要和项目本身实际匹配,如果不匹配,会出现FileNotFoundException

请教一个问题,当我把instrumentation在manifest里面申明以后,运行老报错,说不识别instrumentation这个资源,这个是啥问题,如何解决?

西门吹牛 回复

给一个截图或者log信息看看吧

@chenhengjie123 老大,请教一下,kotlin实现的Android应用也能用Jacoco实现手工测试覆盖率的收集吗?

恒温 将本帖设为了精华贴 25 Nov 08:26

手把手教程,如果能有个视频就更好了

kotlin可以和java无缝配合,所以可以做的

zailushang 回复

如果单单只想测kotlin写的一个sdk,但是这个sdk在另一个项目A中引用,且需要在A中测试sdk的测试覆盖率,不知道有什么好实现的思路?谢谢。

把jacoco的配置写在sdk项目的gradle配置文件中,我们的SDK就是这样做的

simple 回复

👍 正解

zailushang 回复

然后呢?在主项目的build.gradle中不需要配置jacoco的配置吗?能否多讲点大致的步骤?另,我们的sdk是以jar包形式引入主项目中的。

@simple @不二家的小球迷 simple兄弟,可以给小球迷详细说说具体步骤吗?我没这么实践过,只是理论上觉得是可以的


参考一下这个

simple 回复

@zailushang @simple 感谢,我先尝试一下,统计主项目的覆盖率的内容,然后尝试sdk

我们的sdk是插件化多模块的方式,收集覆盖率信息会更麻烦一些,后面可以出一篇文章发到社区里,你试一下先

@simple @zailushang 我在主项目内尝试jacoco,然后已经安装好apk,以后然后启动服务的时候报错了:

adb shell am instrument com.xiaomi.aaa/com.xiaomi.aaa.test.JacocoInstrumentation
android.util.AndroidException: INSTRUMENTATION_FAILED: com.xiaomi.aaa/com.xiaomi.aaa.test.JacocoInstrumentation
at com.android.commands.am.Am.runInstrument(Am.java:956)
at com.android.commands.am.Am.onRun(Am.java:316)
at com.android.internal.os.BaseCommand.run(BaseCommand.java:47)
at com.android.commands.am.Am.main(Am.java:99)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:330)

能把所有报错都发出来吗?这段看不太出来原因

zailushang 回复

这就是所有报错唉,我在命令行已经安装应用成功,然后用这个命令adb shell am instrument命令调起app的时候报错

在manifast中注册InstrumentedActivity了吗? 你是小米的?

zailushang 回复

我不是小米的,我确认了一遍配置,没有配错。我对这个报错很懵逼啊

我参考这个地址 https://stackoverflow.com/questions/14269687/android-util-androidexception-instrumentation-failed ,然后使用命令
adb shell pm list instrumentation,的确没有看到我安装的应用包名。

方便的话,可以把你的代码发过来吗?不然这样不好解决

zailushang 回复

代码应该不行,我还是自己再看看吧,谢谢。

方便私聊不,想跟你QQ一下,872489864方便不?

可以微信,QQ一般不登录,TTMMD155

@simple 方便增加点描述关于sdk覆盖率收集吗?

@simple 大哥,实在想知道,如何统计以jar包引入的sdk覆盖率的统计?能多点介绍吗?

最近在搞绩效,等下周找时间写篇教程哈

simple 回复

简单介绍一下,不需要多复杂的配置。粗略的让我知道有点方向。非常感谢大哥。

请邮件给我,可以对你提供些帮助😃

VPDong 回复

邮件给你什么?

Author only

我找我们团队的专家(VPDong)来帮你解答一下,你俩聊

VPDong 回复

邮件已发。

请问大神,多个*.ec文件需要合并成一个时要怎么做呢

小小南瓜 回复

我没用过,但是jacoco有个命令行工具可以merge,

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 13 Dec 14:44
ivy520 [Topic was deleted] 中提及了此贴 16 Dec 22:55
ivy520 [Topic was deleted] 中提及了此贴 16 Dec 23:53
安涛 [Topic was deleted] 中提及了此贴 21 Dec 16:29

请问从gitlab拉下来的代码,出现这个问题是什么原因?

java.lang.ClassNotFoundException: org.jacoco.agent.rt.RT
java.lang.ClassNotFoundException: org.jacoco.agent.rt.RT
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:400)
at java.lang.Class.forName(Class.java:326)
at com.example.test.JacocoInstrumentation.generateCoverageReport(JacocoInstrumentation.java:72)
at com.example.test.JacocoInstrumentation.onActivityFinished(JacocoInstrumentation.java:94)
at com.example.test.InstrumentedActivity.onDestroy(InstrumentedActivity.java:22)
at android.app.Activity.performDestroy(Activity.java:7082)
at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1154)
at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4322)
at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4353)
at android.app.ActivityThread.-wrap6(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1618)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:163)
at android.app.ActivityThread.main(ActivityThread.java:6385)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:904)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:794)
xiaqing 回复

从报错信息看,是jacoco的类没有找到,在这里直接引入jacoco就行,没有明白你为什么从 gitlab上拉代码

zailushang 专栏文章:[定制触发条件] jacoco 统计 Android 代码覆盖率 中提及了此贴 01 Jan 22:14

请教一下,生成的报告在手机本地可以自动上传吗,如果不能自动上传的话是手工导出来吗,如果app重装了,那这个覆盖率的数据不是没有用了?因为基本测试的时候是多个人用不同的手机在测试,如何体现这个版本的app的覆盖率呢?

迷糊群 回复

可以自动上传,什么方式都可以
也可以手动导出
app重装不重装,跟覆盖率没什么关系吧?本文统计的是代码覆盖率。

迷糊群 回复

不同手机的,可以把不同手机的覆盖率文件下载、合并,然后再计算覆盖率

zailushang 回复

自动上传是用的什么方式呢,能说详细一点吗?感谢

迷糊群 回复

我没有做自动上传,因为每次的覆盖率文件比较大。
大文件建议用FTP上传吧

你好 我在查看覆盖率报告时发现报告没有行覆盖率,请问你有遇到过这关问题吗

小小南瓜 回复

意思是,点进去以后,看不到哪些代码覆盖了,哪些代码没有覆盖吗?

zailushang 回复

是的 ,想看到具体哪些行被覆盖了,但是我的报告只能看到方法级别的覆盖率

小小南瓜 回复

检查一下这两个路径是否是有效的路径

@zailushang 感谢大神!!

InstrumentedActivity是不是只能继承标识为android.intent.action.MAIN 的Activity,不一定是ManiActivity对吧?

不是,用这个方式配置吧,比Instrument方式好,更灵活:https://testerhome.com/articles/17546

是用的你说的这个,请问如何实现增量覆盖,我查了很多资料,还使用过diff-cover工具,还是没实现增量覆盖率

花开 Android 测试增量和全量覆盖率实践 中提及了此贴 06 Dec 16:05
simple 回复

求问,我们Android自动化是每执行一条case就杀进程,杀了进程就没有代码覆盖记录了。如何统计啊?现在全部都做完了发现了这个问题。求助感谢

jiawei0113 回复

杀进程之前,执行覆盖率文件生成逻辑。

zailushang 回复

每次杀进程之前都执行生成ec文件,然后我们大概一百多条case,这样会生成100多个.ec文件?然后再合并?

jiawei0113 回复

这个怎么成本低,怎么来吧

求助,找了一天没有解决这个问题!!!
在单模块项目里,我按照上面操作成功了。在多模块安装项目里,我把上述文件都放到主模块了,然后使用adb shell am instrument出现如下报错:

执行adb shell am instrument 启动app 闪退是啥情况呢?

晓只 回复

看日志

zailushang 回复

嗯 找到问题了。。用错模版了 用了basic的模版。。

adb pull /data/data/xxx/files/coverage.ec(Permission denied) 怎么办,我打印出来在 /data/user/0/xx/files/coverage.ec下,但是还是Permission denied,换成/sdcard/下,加了权限,还是Permission denied😭

这是米8,安卓9系统这样。换个5.0手机,pull文件的时候提示:does not exist

蓝蓝 回复

首先,建议写到sdcard中,如果写到包data路径下,包被卸载后,.ec文件会被删除
关于权限问题,可以放到Cache路径下,getExternalCacheDir() + "/“,我验证了,安卓10也没有权限问题

关于getExternalCacheDir()的具体路径信息,可以打印查看

zailushang 回复

可以的,灰常感谢哇!👍

楼主,我还有个问题呀,android.support.*这种的怎么屏蔽掉?说是gradle excludes不管用导致的?

蓝蓝 回复

看看代码?这样说,不知道具体是什么

zailushang 回复

我使用adb shell am instrument 后app 闪退了

看看logcat的报错

能生产coverage.ec了,但是使用上面方法生产报告方法是失败的

zailushang 回复

使用了你的配置 构建失败 代码提示这几个方法classDirectories sourceDirectories executionData 过时

哦哦,那应该是库方法更新了导致的吧,毕竟这代码两年前的了。
辛苦将替换代码留言一下,我在文章中更新一下,避免误导了后面的同行

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