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

LightingContour發表於2018-10-23

標籤: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

相關文章