专栏文章 [定制触发条件] jacoco 统计 Android 代码覆盖率

在路上 · 2019年01月01日 · 最后由 在路上 回复于 2021年02月26日 · 5370 次阅读

1、前言

之前已经写过一篇关于 [instrument 方式] Jacoco 统计 Android 端手工测试覆盖率,但是在实际使用过程中,自由度不够。
针对不同的测试类型,可能需要不同的写覆盖率文件触发方式。所以才有了本篇:修改源码的方式,定制化写代码覆盖率文件的触发条件。

2、概述

看下文之前,首先考虑一个问题:把大象放进冰箱,一共分几步?
......
同样的,用 Jacoco 统计 Android 代码覆盖率,一共分 6 步:

  1. 编写生成覆盖率文件 coverage.ec 的类和方法
  2. build.gradle(app) 新增 jacoco 插件
  3. 打开覆盖率统计开关
  4. 覆盖率生成条件监听
  5. 安装 debug 版本
  6. 服务器生成 jacoco 报告

3、具体步骤

3.1 编写生成覆盖率文件 coverage.ec 的类和方法

src 目录下,新建一个 jacocotest package,放入 JacocoUtils.java 测试类

代码见:

package com.keniu.security.main.jacocotest;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class JacocoUtils {
    static String TAG = "JacocoUtils";

    //ec文件的路径
    private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/cleanmaster_coverage.ec";

    /**
     * 生成ec文件
     *
     * @param isNew 是否重新创建ec文件
     */
    public static void generateEcFile(boolean isNew) {
        String currentTimeStr = String.valueOf(System.currentTimeMillis());
//        DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/cleanmaster_coverage" + currentTimeStr + ".ec";
        Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH);
        OutputStream out = null;
        File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);

        //create and delete coverage.ec
        try {
            if (isNew && mCoverageFilePath.exists()) {
                Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件");
//                mCoverageFilePath.delete();
            }
            if (!mCoverageFilePath.exists()) {
                mCoverageFilePath.createNewFile();
            }
            out = new FileOutputStream(mCoverageFilePath.getPath(), true);

            //反射:获取org.jacoco.agent.rt.IAgent
            Object agent = Class.forName("org.jacoco.agent.rt.RT"   )
                    .getMethod("getAgent")
                    .invoke(null);

            //反射:getExecutionData(boolean reset),获取当前执行数据,以jacoco二进制格式转储当前执行数据
            // getExecutionData(boolean reset),reset如果为true,则之后清除当前执行数据
            out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
                    .invoke(agent, false))  ;

        } catch (Exception e) {
            Log.e(TAG, "generateEcFile: " + e.getMessage());
        } finally {
            if (out == null)
                return;
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

3.2 build.gradle(app) 新增 jacoco 插件

apply plugin: 'jacoco'

jacoco {
    toolVersion = '0.7.4+'
}

如图所示:

3.3 打开覆盖率统计开关

说明:也可选择 release 版本打开覆盖率开关。

buildTypes {
    debug {
        /**打开覆盖率统计开关*/
 testCoverageEnabled = true
    }
}

如图所示:

3.4 覆盖率生成条件监听

触发条件可以根据覆盖率统计场景,选择合适的一个。

(1)可新建线程定时触发

线程代码见:

package com.keniu.security.main.jacocotest.handler;

import android.os.Handler;
import android.os.Message;

import com.keniu.security.main.jacocotest.JacocoUtils;

public class MyThread implements Runnable {
    Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            // 要做的事情
            JacocoUtils.generateEcFile(true);
        }
    };

    boolean jacocoBoolean = true;

    @Override
    public void run() {
        // TODO Auto-generated method stub
        while (jacocoBoolean) {
            try {
                Thread.sleep(10000);// 线程暂停10秒,单位毫秒
                Message message = new Message();
                message.what = 1;
                handler.sendMessage(message);// 发送消息
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    public void end() {
        jacocoBoolean = false;
    }
}

在 MainActivity 中新建线程并启动:

MyThread jacocoThread = new MyThread();
@Override
protected void onCreate(Bundle savedInstanceState) {
    ......
    //jacoco定时任务开始
 new Thread(jacocoThread).start();
   ......      
}

(2)加在监听设备按键的地方,如果连续 2 次点击设备 back 键,app 已置于后台,则调用生成覆盖率方法。

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
....
JacocoUtils.generateEcFile(true);
}
}

3.5 安装 debug 版本

build / Rebuild Project

安装 app-debug.apk 至手机

3.6 服务器生成 jacoco 报告

(1)在 build.gradle(project) 新增 jacocoTestReport


def coverageSourceDirs = [
        './src/'
]

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/',
            excludes: ['**/R*.class',
                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
 ])
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/outputs/cleanmaster_coverage.ec")

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

(2)上传 ec 文件并生成报告

然后设备上传 ec 文件到 Android 工程的 $buildDir/outputs/code-coverage/connected 目录下,并依次执行

gradle createDebugCoverageReport
gradle jacocoTestReport

3.7 可能遇到问题

(1)rebuild Project 时卡在 app:transformClassesWithDexForDebug

解决方法:

按照如下方式设置即可:

debug {
    buildConfigField "boolean", "FORTEST", "false"
 // 启动代码压缩
 minifyEnabled true
 // 启用资源压缩
 shrinkResources true
 signingConfig signingConfigs.debug
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'kbrowser.pro'
 /**打开覆盖率统计开关*/
 testCoverageEnabled = true
}

如图所示:

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

卡在 app:transformClassesWithDexForDebug 真的卡了好久好久,之前一直以为是因为集成的工程太复杂导致,请问到底是什么原因呢?
我们是把 minifyEnabled 设置成 true 就可以成功了

剪烛 回复

我这边猜测可能是内存资源紧张导致的速度慢,所以打开代码压缩和资源压缩(开发也这么认为),后期可以深入研究一下

你好 麻烦加一下 qq 有指教 谢谢 371844911

匿名 #4 · 2019年04月11日

你好,我想请教一下,可以实时获取当前代码的覆盖率么? 楼主加一下我吧 747598346

实时获取的意思是?你可以用 test with coverage 测试

gradle createDebugCoverageReport
根本就没有这个 Task,你怎么执行的~~~

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
....
JacocoUtils.generateEcFile(true);
}
}
```你好我想问一下这里只监听了一次back键吧而且如果当前页面是下级页面即使点击两次back键也可能只是停留在某个上级页面不一定会进入后台
6dingdong6 回复

嗯嗯,是的,可以再加上 activity 的判断

下面这两步编译失败什么原因?$buildDir/outputs/code-coverage/connected 这个目录是手动创建的吗?
然后设备上传 ec 文件到 Android 工程的 $buildDir/outputs/code-coverage/connected 目录下,并依次执行
gradle createDebugCoverageReport
gradle jacocoTestReport

三个L 回复

发一下失败的 log

11楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册