Android Demo:手工覆盖率(AS3.2)use Jacoco

2,849 阅读7分钟

标签:Android Jacoco 手工覆盖率

作者:LightingContour

GitHub

刚好遇到GitHub宕机…… 不过还好,现在可以了
地址:https://github.com/LightingContour/LC-JacocoSample

引言

笔者这几天搞了下Android覆盖率,使用的Jacoco,打算做个手工覆盖率的Demo。
一开始很开心啊,看到有各种教程,很简单的样子(误)。但是一步步做下去,遇到了很多问题很多坑,一卡卡一天。
现在总算折腾出来了。这里做一个最简洁的手工覆盖率Demo教程。写给之后来这条路的大家,也是巩固下自己学到的东西。

简介

本篇主要介绍如何写出通过Jacoco实现的覆盖率Demo
Demo主Activity中有三个Button,前两个分别都只是更改TextView内容。第三个点击会导出覆盖率。
另外为测试覆盖率,还写了个小小的彩蛋隐藏在前两个Button代码中。
配套工具版本:Android Studio3.2

需要Get的知识

Jacoco简介

JaCoCo是一个开源的覆盖率工具(官网地址:http://www.eclemma.org/JaCoCo/),它针对的开发语言是java,其使用方法很灵活,可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。

很多第三方的工具提供了对JaCoCo的集成,如sonar、Jenkins等。

Instrumentation

Instrumentation和Acitivity很类似,但是没有图形界面。
可以把它理解为用于监控其他类的工具类。

继承自以下教程

https://blog.csdn.net/qq_27459827/article/details/79514941?utm_source=blogxgwz0
https://blog.csdn.net/niubitianping/article/details/52918809
https://blog.csdn.net/itfootball/article/details/45644159

设计思路

1.先写一个最基本的Activity配Xml
2.在这个Activity的基础上添加存储权限,我们会将覆盖率文件保存到SD卡上
3.添加覆盖率代码

跟我一起动手做

创建基础程序

1.创建一个Project,名为LC-JacocoSample。在引导页面选择Empty Activity。

1-创建.png
2.Gradle Sync老是转圈圈?
请在Project的build.gradle中新增阿里云国内maven地址。
然后,Sync一下,所有依赖就会很快Down下来啦。

另外需要注意,这里请使用3.1.3版本的Gradle。后面会有坑~!

2-Project Gradle中增加阿里云国内maven地址.png

3.更改MainActivity位置。在Android视图的com.lightingcontour.jacocotry下新增以下Package:app、test、Utils。
这是为了之后做准备。Utils用来存权限获取相关文件,test用来存覆盖率文件。然后将MainActivity拖到app package下。

更改Activity位置

4.XML布局文件更新
三个Button,很简单的配置

3-布局配置.png

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".app.MainActivity">

    <Button
        android:id="@+id/Btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="24dp"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/Test1" />

    <TextView
        android:id="@+id/Test1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TEST1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.126"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.186" />

    <TextView
        android:id="@+id/Test2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="TEST2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.737"
        app:layout_constraintStart_toEndOf="@+id/Test1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.176" />

    <Button
        android:id="@+id/Btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="20dp"
        android:text="Button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/Test2" />

    <Button
        android:id="@+id/Btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

5.MainActivity中新增代码,绑定Button,点击Button时会更改TextView的值等

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    //定义layout中所用组件
    public TextView A,B;

    private int AClickedTime = 0;
    private boolean easterEgg = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //赋值、绑定layout组件
        A = (TextView) findViewById(R.id.Test1);
        B = (TextView) findViewById(R.id.Test2);

        findViewById(R.id.Btn1).setOnClickListener(this);
        findViewById(R.id.Btn2).setOnClickListener(this);
        findViewById(R.id.Btn3).setOnClickListener(this);


    }

    @Override
    public void onClick(View v) {
        switch (v.getId())
        {
            case R.id.Btn1:
                Toast.makeText(this,"点击了第一个按钮",Toast.LENGTH_SHORT).show();
                A.setText("点击了第一个按钮");

                //设定彩蛋:点击了第一个按钮三次,flag为true
                if (AClickedTime < 3)
                {
                    AClickedTime++;
                }else {
                    easterEgg = true;
                }
                break;
            case R.id.Btn2:
                Toast.makeText(this, "点击了第二个按钮", Toast.LENGTH_SHORT).show();
                B.setText("点击了第二个按钮");

                //设定彩蛋:flag为true时,执行以下操作
                if (easterEgg == true)
                {
                    A.setText("恭喜进入彩蛋");
                    B.setText("恭喜进入彩蛋");
                }
                break;
            case R.id.Btn3:
                Toast.makeText(this,"点击了第三个按钮",Toast.LENGTH_SHORT).show();
                break;
        }

    }
}

6.测试一下,Build-Run。程序运行成功~第一部分完成!

虚拟机演示

添加SD卡存储权限

在进行覆盖率代码编写之前,我们还需要先搞定SD卡存储权限。
我们要先将覆盖率文件放到手机的SD卡中。然而大家知道,从Android6.0开始,不仅仅要在manifest中添加权限,还要在程序中去动态申请获取。那么开始吧~
1.修改manifest

Manifest中进行修改.png

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

2.新增PermissionUtils用于获取存储权限

public class PermissionUtils {
    // Storage Permissions 存储权限
    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE};

    /**
     * Checks if the app has permission to write to device storage
     * If the app does not has permission then the user will be prompted to
     * grant permissions
     *
     * 检查App是否有SD卡的写入权限
     * 如果没有,让系统提醒授予
     *
     * @param activity
     */
    public static void verifyStoragePermissions(Activity activity) {
        // Check if we have write permission
        try {
            int permission = ActivityCompat.checkSelfPermission(activity,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE);
            if (permission != PackageManager.PERMISSION_GRANTED) {
                // We don't have permission so prompt the user
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
                        REQUEST_EXTERNAL_STORAGE);
            }
        } catch (Exception e){
            e.printStackTrace();
        }

    }
}

3.在MainActivity启动时添加权限获取
onCreate方法中调用

//动态申请SD卡读取权限
PermissionUtils.verifyStoragePermissions(this);

调用Jacoco

1.1在test Package中新增FinishListener.java

在这里添加FinishListener

package com.lightingcontour.lc_jacocosample.test;

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

1.2在test Package中新增InstrumentationActivity.java

package com.lightingcontour.lc_jacocosample.test;

import android.util.Log;

import com.lightingcontour.lc_jacocosample.app.MainActivity;

public class InstrumentedActivity extends MainActivity {
    public static String TAG = "InstrumentedActivity";

    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();
        }
    }
}

1.3在test Package中新增JacocoInstrumentation.java

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;
    //LOGD 调试用布尔
    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() + "/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();
    }

    public void onStart() {
        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 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 (FileNotFoundException e) {
                    Log.d(TAG, e.toString(), e);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } finally {
                    if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void UsegenerateCoverageReport() {
        generateCoverageReport();
    }

    private boolean setCoverageFilePath(String filePath){
        if (filePath != null && filePath.length() > 0) {
            mCoverageFilePath = filePath;
        }
        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_IDENTIFIER,"\nError: " + msg);
    }

    @Override
    public void onActivityFinished() {
        if (LOGD) {
            Log.d(TAG,"onActivityFinished()");
        }
        finish(Activity.RESULT_OK,mResults);
    }

    @Override
    public void dumpIntermediateCoverage(String filePath) {
        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);
        }
    }
}

2.更改App Model的Gradle文件
2.1新增使用Jacoco

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.6.201602180812"
}

2.2在Manifest中新增Jacoco权限

    <!-- Jacoco权限-->
    <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" />

2.3新增task,用于将应用跑出来的覆盖率ec文件转换为html可读文档

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: '../app/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('?', '$'))
            }
        }
    }
}

3.在MainActivity中增加调用生成Jacoco覆盖率
3.1新增调用JacocoInstrumentation

import com.lightingcontour.lc_jacocosample.test.JacocoInstrumentation;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

//新增下面这个调用
public JacocoInstrumentation jacocoInstrumentation = new JacocoInstrumentation();

点击Button3的时候,就调用生成覆盖率方法

case R.id.Btn3:
                Toast.makeText(this,"点击了第三个按钮",Toast.LENGTH_SHORT).show();
                jacocoInstrumentation.UsegenerateCoverageReport();
                break;

准备完成!
那么梳理一下
我们在test Package中新增了三个文件用于Jacoco测试。
在Manifest中新增了权限。
在Gradle中新增了用于将ec文件生成html覆盖率报告的task。
在MainActivity中新增了对JacocoInstrumentation的调用以及点击第三个Button时生成覆盖率ec文件。
接下来就是开跑了!~

输出覆盖率文件

1./gradlew(windows用gradlew) installDebug 也可以用gradle视图中的installDebug
记得连接真机或者AVD哈

命令符.png
Gradle视图.png
2.需要adb,没有的装一下
命令行中输入

adb shell am instrument -w -r  com.lightingcontour.lc_jacocosample/.test.JacocoInstrumentation

真机或者AVD会弹出做好的App,操作操作,点击下几个按钮之类的。
最后点击下Button3,就会导出我们的覆盖率文件了。

点击完成后就可以退出App了,这样后命令行中也会提示退出。

退出提示.png

3.使用adb命令,复制到我们的电脑中。
我这儿用的是mac,直接复制到桌面上。

adb pull mnt/sdcard/coverage.ec ~/Desktop/123.ec

Pull成功后,将得到的文件放到task中指定的
$buildDir/outputs/code-coverage/connected/coverage.ec中,
也就是**.../LC-JacocoSample/app/build/outputs/code-coverage/connected**
然后使用Gradle视图中的jacocoTestReport或者命令行,都行

生成html文件.png

最后,生成的报告在
.../LC-JacocoSample/app/build/reports/jacoco/jacocoTestReport/html

覆盖率报告.png

恭喜大家,完成啦~

之后会源码上传到Github,欢迎来点个Star!
有什么问题可以尽量在github中提issue,我会在上面看。

遇到的坑记录

  1. jacocoTestReport无输出-app/build/intermediates/classes无内容
    原因:gradle太新了,编译文件变更-Project:JacocoTry用gradle版本改成3.1.3
  2. Unable to read execution data file …/coverage.ec
    解决方案:改toolVersion-jacoco {toolVersion = "0.7.6.201602180812"}
    在其他帖子上也看到改到其他版本的……大家如果遇到了可以尝试下
    https://blog.csdn.net/roxxo/article/details/77720300#commentBox

参考资料

https://blog.csdn.net/qq_27459827/article/details/79514941?utm_source=blogxgwz0
https://blog.csdn.net/niubitianping/article/details/52918809
https://blog.csdn.net/itfootball/article/details/45644159

https://blog.csdn.net/o279642707/article/details/54576307

https://cloud.tencent.com/developer/article/1038055