测试覆盖率 [instrument 方式] Jacoco 统计 Android 端手工测试覆盖率

zailushang · 2018年10月09日 · 最后由 zailushang 回复于 2018年12月10日 · 最后更新自管理员 恒温 · 3537 次阅读
本帖已被设为精华帖!

示例JacocoTestForAndroid见:https://github.com/OnTheWay111/JacocoTestForAndroid

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

打开报告后,如图所示:

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

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

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

西门吹牛 回复

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

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

恒温 将本帖设为了精华贴 11月25日 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 回复

邮件给你什么?

仅楼主可见

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

VPDong 回复

邮件已发。

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

小小南瓜 回复

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

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
ivy520 [该话题已被删除] 中提及了此贴 12月16日 22:55
ivy520 定向班第一期_shell 课程实战_20181216 中提及了此贴 12月16日 23:53
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册