Android-jacoco代碼覆蓋率:單元測試覆蓋率+功能測試覆蓋率


參考:https://docs.gradle.org/current/dsl/org.gradle.testing.jacoco.tasks.JacocoCoverageVerification.html

gradle庫下載:https://maven.aliyun.com/mvn/view

案例參考來源:https://www.jianshu.com/p/1a4a81f09526

https://www.jianshu.com/p/1a4a81f09526

其他:https://testerhome.com/topics/8329

 

這幾天折騰了很久,主要是現在的案例都是基於gradle3.1.3版本,我不想用舊版本的,查了一些資料,自己改了下代碼,可以用了。

前情:

之前聽說Android可以用jacoco+monkey做代碼覆蓋率測試,以前只做過一個spring的jacoco的單元測試覆蓋率的demo,沒想過Android可以將功能和jacoco聯合在一起,這幾天很閑就搞了一下。

准備工作:

要有Android項目源碼,不用修改項目主體的核心代碼,但是需要寫一些jacoco的代碼,主要是利用instrument在acitivity結束時記錄代碼覆蓋率;

具體內容分兩塊:

一,Android項目的單元測試代碼覆蓋率:

利用AndroidStudio自帶的task來查看當前AndroidTest文件夾下的單元測試用例覆蓋率情況

編輯build.gradle

android {
    ...
    defaultConfig {
         ...
        testInstrumentationRunnerArguments clearPackageData: 'true'
//        執行instrumentation測試時清除緩存
    }
    buildTypes {
        debug {
            testCoverageEnabled = true
            /**打開覆蓋率統計開關
             */
        }
    }

  

安裝debug包

 

 

執行AndroidTest的覆蓋率測試並輸出報告

 

 

執行日志是這樣的:

  這里摘取的是執行AndroidTest單元測試的片段,通過adb發送instrument命令到手機,執行測試,獲取覆蓋率數據,並從手機中down下來:

Executing tasks: [createDebugAndroidTestCoverageReport]
...
> Task :app:connectedDebugAndroidTest
...
05:40:39 V/ddms: execute: running am instrument -w -r   -e coverageFile /data/data/com.patech.testApp/coverage.ec -e coverage true -e clearPackageData true com.patech.testApp.test/androidx.test.runner.AndroidJUnitRunner
...
05:40:41 V/InstrumentationResultParser: com.patech.testApp.EspressoTest:
...
05:40:58 V/InstrumentationResultParser: Time: 17.669
05:40:58 V/InstrumentationResultParser: 
05:40:58 V/InstrumentationResultParser: OK (5 tests)
05:40:58 V/InstrumentationResultParser: 
05:40:58 V/InstrumentationResultParser: 
05:40:58 V/InstrumentationResultParser: Generated code coverage data to /data/data/com.patech.testApp/coverage.ec
05:40:58 V/InstrumentationResultParser: INSTRUMENTATION_CODE: -1
...
05:40:59 I/XmlResultReporter: XML test result file generated at D:\androidStudio\MyApplication\app\build\outputs\androidTest-results\connected\TEST-VOG-AL10 - 9-app-.xml. Total tests 5, passed 5, 
05:40:59 V/ddms: execute 'am instrument -w -r   -e coverageFile /data/data/com.patech.testApp/coverage.ec -e coverage true -e clearPackageData true com.patech.testApp.test/androidx.test.runner.AndroidJUnitRunner' on 'APH0219430006864' : EOF hit. Read: -1
...
05:40:59 D/com.patech.testApp.coverage.ec: Downloading com.patech.testApp.coverage.ec from device 'APH0219430006864'
...

  

執行完畢后查看build下的reports的詳情

 

 

 

二.編寫jacoco+instrument的代碼,執行功能測試后,在本地生成ec文件,傳到pc端后解析成html格式,查看功能測試操作的代碼覆蓋率執行情況

1.編寫FinishListener接口

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

  

編寫jacocoInstrumentation方法,實現上面這個接口,網上抄來的,實現了執行完成后生成覆蓋率文件並保存到手機本地:

package com.patech.test;

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 com.patech.testApp.InstrumentedActivity;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;

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.修改AndroidManifest.xml文件,添加往手機讀寫的權限,以及instrument的設置,該標簽與application標簽同級:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 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" />

    <instrumentation
        android:name="com.patech.test.JacocoInstrumentation"
        android:handleProfiling="true"
        android:label="CoverageInstrumentation"
        android:targetPackage="com.patech.testApp" />

3.編寫jacoco.gradle,用於解析ec,轉換成html或者其他格式的報告:

apply plugin: 'jacoco'
//https://docs.gradle.org/current/userguide/jacoco_plugin.html
jacoco {
    toolVersion = "0.8.4"
}
task jacocoTestReport(type: JacocoReport) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug",
//            includes: ["**/*Presenter.*"],
            excludes: ['**/R*.class',
                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
            ])//指定類文件夾、包含類的規則及排除類的規則,這里我們生成所有Presenter類的測試報告
    def coverageSourceDirs = "${project.projectDir}/src/main/java" //指定源碼目錄
    def reportDirs="$buildDir/outputs/reports/jacoco/jacocoTestReport"
    reports {
        xml.enabled = true
        html.enabled = true
    }
//    destinationFile=file(reportDirs)
    classDirectories = files(debugTree)
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
}

4.連接手機,安裝apk后執行adb語句,通過jacoco開啟應用:

adb shell am instrument -w -r  com.patech.testApp/com.patech.testcoverage.test.JacocoInstrumentation

5.可以在手機上開始做功能測試了,測試完畢后導出ec文件:

adb pull mnt/sdcard/coverage.ec C:\Users\user\Desktop\testReport\jacoco

6.將ec文件放入build/outputs/code-coverage/connected下

 

 執行jacocoTestReport的task

 

 在build/reports/jacoco/jacocoTestReport下查看解析的報告

 

查看報告:

 

 

 

 

 

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM