最近在做android测试时代码的覆盖率,网上查了好长一段时间,遇到了很多坑,在这里记录一下。

1、jacoco介绍:

      jacoco是Java Code Coverage的缩写,顾名思义,是Java代码覆盖率统计的主流工具之一。关于jacoco的原理可以移步jacoco原理介绍。在安卓项目的代码覆盖率统计使用了jacoco的离线插桩方式,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试(单元测试、UI测试或者手工测试等)插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。

2、手动测试生成coverage.ec文件

    这个网上有很多的例子,我这里也直接照搬过来。

在不修改android源码的情况下,在src/main/java 里面新增一个test目录 里面存放3个文件:FinishListener、InstrumentedActivity、JacocoInstrumentation,这三个文件的源码在网上有很多,我这里后面也会贴出来。

先看下目录结构:

Android 覆盖so执行恶意代码 安卓代码覆盖率_android

FinishListener源码:

public interface FinishListener {
    void onActivityFinished();
    void dumpIntermediateCoverage(String filePath);
}

InstrumentedActivity源码: 

import com.netease.coverage.jacocotest1.MainActivity;
public class InstrumentedActivity extends MainActivity {
    public FinishListener finishListener ;
    public void  setFinishListener(FinishListener finishListener){
        this.finishListener = finishListener;
    }

    @Override
    public void onDestroy() {
        if (this.finishListener !=null){
            finishListener.onActivityFinished();
        }
        super.onDestroy();
    }

}

JacocoInstrumentation源码: 

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

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.isFile() && file.exists()){ 
            if (file.delete()){
                System.out.println("file del successs");
            }else {
                System.out.println("file del fail !");
            }
        }
        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() {
        System.out.println("onStart def");
        if (LOGD)
            Log.d(TAG, "onStart()");
        super.onStart();

        Looper.prepare();
        InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
        activity.setFinishListener(this);
    }

    private boolean getBooleanArgument(Bundle arguments, String tag) {
        String tagString = arguments.getString(tag);
        return tagString != null && Boolean.parseBoolean(tagString);
    }

    private void generateCoverageReport() {
        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);
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

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

    private boolean setCoverageFilePath(String filePath){
        if(filePath != null && filePath.length() > 0) {
            mCoverageFilePath = filePath;
            return true;
        }
        return false;
    }

    private void reportEmmaError(Exception e) {
        reportEmmaError("", e);
    }

    private void reportEmmaError(String hint, Exception e) {
        String msg = "Failed to generate emma coverage. " + hint;
        Log.e(TAG, msg, e);
        mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
                + msg);
    }

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

    @Override
    public void dumpIntermediateCoverage(String filePath){
        // TODO Auto-generated method stub
        if(LOGD){
            Log.d(TAG,"Intermidate Dump Called with file name :"+ filePath);
        }
        if(mCoverage){
            if(!setCoverageFilePath(filePath)){
                if(LOGD){
                    Log.d(TAG,"Unable to set the given file path:"+filePath+" as dump target.");
                }
            }
            generateCoverageReport();
            setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
        }
    }
}

在app下面的build.gradle内需添加内容:

apply plugin: 'jacoco'
jacoco {
    toolVersion = "0.7.4+"
}
buildTypes {
    debug {
        /**打开覆盖率统计开关**/
        testCoverageEnabled = true
    }
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

与android平级 添加: 

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('$$', '$'))
            }
        }
    }
}

AndroidManifest.xml添加配置:

在manifest标签中添加权限: 

<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

在application标签中添加activity

<activity android:label="InstrumentationActivity" android:name="com.netease.coverage.test.InstrumentedActivity" />

与application平级 添加JacocoInstrumentation声明(这里targetPackage 会报错忽略):

<instrumentation
    android:handleProfiling="true"
    android:label="CoverageInstrumentation"
    android:name="com.netease.coverage.test.JacocoInstrumentation"
    android:targetPackage="com.netease.coverage.jacocotest1"/>  <!-- 项目名称 -->

完整结构图如下:

Android 覆盖so执行恶意代码 安卓代码覆盖率_.net_02

其实写到这里基本已经完成了,可以安装包进行测试了(我用的是模拟器测试的)。

安装包需要注意:需要用installDebug 进行安装(刚开始路径可能不一样,可能在other里面)

Android 覆盖so执行恶意代码 安卓代码覆盖率_Android 覆盖so执行恶意代码_03

apk包安装好以后,用adb 去启动程序进行测试,我这里执行的是:

adb shell am instrument com.netease.coverage.jacocotest1/com.netease.coverage.test.JacocoInstrumentation

(adb shell am instrument 项目所在的包/JacocoInstrumentation文件路径)

测试完后,按返回键 将app切入后台,即可在device monitor 的/data/data/项目文件/files 下找到coverage.ec文件,将这个文件导出。

3、生成报告html

先在gradle里面执行createDebugCoverageReport 在app/build/outputs里面生成code-coverage文件

将code-coverage/connected里面原始的ec文件删除,并加入刚才生成的coverage.ec文件。

执行gradle jacocoTestReport 生成报告,路径:app/build/reports/jacoco/jacocoTestReport/html/index.html

遇到的问题总结:

1、未找到org.jacoco.agent.rt.RT :

因为gradle已经集成了jacoco,但是作为刚接触jacoco的新手时,遇到这样的错误,只好自己去jacoco官网下了包 导入进来发现了另一个问题:JaCoCo agent not started.  还特意去查看源码:

Android 覆盖so执行恶意代码 安卓代码覆盖率_android_04

返回的是jvm中的agent,但是显然是并没有agent在jvm中导致的错误。在网上找了好久都没能找到问题的解决方案,后来仔细想了想 可能是启动方式不对?想了想第一次是用robotium写了一个例子是成功得到了报告,在gradle中找到了用installDebug的安装方式,试了试果然可以生产ec文件。