白盒测试 Android jacoco 代码覆盖率测试入门

xinxi · November 28, 2018 · Last by 999 replied at January 11, 2019 · 6371 hits
本帖已被设为精华帖!

前言

最近同事搞了一个基于 jacoco 统计 Android 代码覆盖率测试的功能,可以统计每天手工测试的代码覆盖率.抱着好奇的心态,自己也学习一下 jacoco,陆陆续续搞了三天终于有点结果了.

本文介绍仅仅在源码中加入少量代码就可以完成代码覆盖率覆测试.

代码配置

build.gradle

在 app 目录下的 build.gradle 配置 jacoco

apply plugin: 'jacoco'
jacoco {
    toolVersion = "0.7.9"
}


dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.1.1'
    compile 'org.jacoco:org.jacoco.core:0.7.9'
    compile 'com.android.support.constraint:constraint-layout:+'
}

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/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/classes/").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

写入 ec 文件

自定义一个 JacocoUtils 类,可以根据反射拿到方法、类的执行代码,写入到.ec 文件

  public static void generateEcFile(boolean isNew) {
//        String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec";
        Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH);
        OutputStream out = null;
        File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
        try {
            if (isNew && mCoverageFilePath.exists()) {
                Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的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);

            out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
                    .invoke(agent, false));
            Log.d(TAG,"写入" + DEFAULT_COVERAGE_FILE_PATH + "完成!" );
        } catch (Exception e) {
            Log.e(TAG, "generateEcFile: " + e.getMessage());
            Log.e(TAG,e.toString());
        } finally {
            if (out == null)
                return;
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();

            }
        }
    }

使用 Application 生成 ec

继承 Application 类,重写 onTrimMemory 方法,系统会根据不同的内存状态来回调

系统提供的回调有:
Application.onTrimMemory()
Activity.onTrimMemory()
Fragement.OnTrimMemory()
Service.onTrimMemory()
ContentProvider.OnTrimMemory()
OnTrimMemory的参数是一个int数值,代表不同的内存状态:
TRIM_MEMORY_COMPLETE:内存不足,并且该进程在后台进程列表最后一个,马上就要被清理
TRIM_MEMORY_MODERATE:内存不足,并且该进程在后台进程列表的中部。
TRIM_MEMORY_BACKGROUND:内存不足,并且该进程是后台进程。
TRIM_MEMORY_UI_HIDDEN:内存不足,并且该进程的UI已经不可见了。 

可以根据 level == TRIM_MEMORY_UI_HIDDEN 来确定 app 已经至于后台,此时调用 generateEcFile 方法.

//判断是否是后台
@Override
public void onTrimMemory(int level) {
    super.onTrimMemory(level);
    if (level == TRIM_MEMORY_UI_HIDDEN) {
        isBackground = true;
        notifyBackground();
    }
}

private void notifyBackground() {
    // This is where you can notify listeners, handle session tracking, etc
    Log.d(TAG, "切到后台");
    JacocoUtils.generateEcFile(true);
}

操作步骤

给予 app 读写 sdcard 权限

因为我的是简单的 demo 代码,启动没有弹窗询问读写 sdcard 权限,
Android6.0 以后是动态获取权限了,所以需要手动去设置中把 sdcard 权限打开,实际项目应该不存在手动打开的步骤.

手工执行

安装 app->操作 app->app 至于后台->分析 ec 文件.

自动化执行

可以结合 monkey 和 UI 自动化,我简单写了个 shell 脚本.从编译 app、启动 app、app 至于后台、自动展示 jacoco 报告

#!/usr/bin/env bash
#当前在环境为Project/app目录

apk_path=`pwd`/app/build/outputs/apk/app-debug.apk
report_path=`pwd`/reporter/index.html

echo "打包app"
gradle assembleDebug
adb uninstall com.weex.jasso
echo "安装app"
adb install ${apk_path}
echo "启动app"
adb shell am start -W -n com.weex.jasso/.Test1Activity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -f 0x10200000
sleep 2
echo "关闭app"
adb shell am force-stop com.weex.jasso

rm -rf `pwd`/new.ec
rm -rf `pwd`/report
adb pull /sdcard/jacoco/coverage.ec `pwd`/new.ec

macaca coverage -r java -f `pwd`/new.ec -c `pwd`/app/build/intermediates/classes/debug -s `pwd`/app/src/main/java --html `pwd`/reporter
echo "jacoco报告地址:"${report_path}
open -a "/Applications/Safari.app" ${report_path}

效果


macaca coverage 生产报告

使用 gradle 的 jacocoTestReport 也可以生产报告,也是大多人使用的方式,本文就不做介绍了,主要介绍使用 macaca coverage 方法.

macaca coverage 可以生成 jacoco 报告,不仅可以生成 Android 项目,也可以生产 iOS、web 项目.具体使用请查看https://macacajs.github.io/zh/coverage.

安装macaca-coverage命令:

npm i macaca-cli -g
macaca coverage -h
npm i macaca-coverage --save-dev
macaca coverage命令:
macaca coverage -r java -f `pwd`/new.ec -c `pwd`/app/build/intermediates/classes/debug -s `pwd`/app/src/main/java --html `pwd`/reporter

项目代码

https://github.com/xinxi1990/jacocodemo.git

在项目根目录有个jacaco_test.sh,可以完成自动化测试.

学习帖

https://blog.csdn.net/qq_27103959/article/details/74549964

https://blog.csdn.net/qq_28709925/article/details/51242081

https://www.cnblogs.com/xiajf/p/3993599.html

共收到 10 条回复 时间 点赞
思寒_seveniruby 将本帖设为了精华贴 01 Dec 18:36
2Floor has deleted

666 学习了

楼主请问一下:

手工执行

安装 app->操作 app->app 至于后台->分析 ec 文件.

这里操作 app 需要通过 InstrumentationTestRunner 来启动吗?我这边实践,手动启动后,就会报错 jacoco 的实例找不到,但是通过 InstrumentationTestRunner 启动就没有问题。请问楼主知道这可能是什么原因吗?

剪烛 回复

不需要啊,InstrumentationTestRunner 需要在代码里边配置,很麻烦的.直接手工启动 app 就行.

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

想哭……楼主我用您的 demo 跑了,除了改了 gradle 插件的版本和 gradle 版本以外,基本都没改。但是也还是出现了(我自己的 demo 也是如此)

12-12 18:39:11.801 4020-4020/com.monkey.myapplication E/JacocoUtils: generateEcFile: java.lang.ClassNotFoundException: org.jacoco.agent.rt.RT
12-12 18:39:11.801 4020-4020/com.monkey.myapplication E/JacocoUtils: generateEcFile: org.jacoco.agent.rt.RT
12-12 18:39:11.801 4020-4020/com.monkey.myapplication E/JacocoUtils: generateEcFile: [Ljava.lang.StackTraceElement;@7762a11

想死的心情都有了。。。。。您知道可能是什么原因吗?

小小南瓜 回复

暂时还不知道怎么合并,ec 中记录很多信息,怕是再合并出问题了

剪烛 回复

gralde 里边有个 debug 开关 你看开了吗?

xinxi 回复

我好像确定了,我使用 gradle plugin 2.3.1 gradle 3.3 这个配套是有这个问题,但是我换成 gradle plugin 2.2.2 gradle 2.14.1 就可以了,基本确定是 gradle 版本引起的。

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

同求怎样合并多个 coverage.ec 文件

感谢楼主分享,经过尝试有两个地方需要注意下 sdcard 中要有 jacoco 文件夹否则创建 ec 文件时会抛异常,build.gradle 里面需要添加 testCoverageEnabled = true,否则也会报异常

AppetizerIO App 稳定性测试面面观 中提及了此贴 26 Jun 12:44
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up