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
}

如图所示:


↙↙↙阅读原文可查看相关链接,并与作者交流