1. 程式人生 > >Jacoco在Android系統應用測試中覆蓋率一直為0的解決方案

Jacoco在Android系統應用測試中覆蓋率一直為0的解決方案

問題

普通應用Gradle配置Jacoco,執行createDebugAndroidTestCoverageReport,能夠正常輸出覆蓋率報告,報告路徑為:
build/reports/coverage/debug/index.html。檢視build/outputs/code-coverage/connected/*-coverage.ec,存在執行覆蓋資料。

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "example.api.com.myapplication2"
        minSdkVersion 24
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug{
            testCoverageEnabled true
        }
    }
 }

在AndroidManifest中增加android:sharedUserId=”android.uid.system”,輸出報告覆蓋率一直為0。檢視build/outputs/code-coverage/connected/*-coverage.ec 一直為空。

Jacoco原理

參考:https://blog.csdn.net/allan_shore_ma/article/details/80053340
簡單來說:
debug模式下增加testCoverageEnabled true 後Gradle自動利用 jacoco外掛在生成最終的目標檔案之前,對源app Class檔案進行插樁,生成最終的目標檔案,執行目標檔案以後得到覆蓋執行資料。通過執行createDebugAndroidTestCoverageReport,在測試程式結束後會通過反射呼叫org.jacoco.agent.rt.RT.getAgent().getExecutionData(false) 獲取到二進位制資料並將此檔案資料同步到uild/outputs/code-coverage/connected/${deviceName}-coverage.ec中。然後Gralde將此檔案與build/intermediates/classes檔案進行比較標記出類和行號,同時與src對比,輸出報告到build/reports/coverage/debug/index.html

解決辦法

因為程式碼插樁原理都是一樣的,執行後也會有jacoco的覆蓋資料資訊。但實際系統應用獲取到的coverage.ec為空,可以猜測以為許可權問題,此資料無法直接匯入到build/outputs/code-coverage/connected/*-coverage.ec。系統應用以及它的測試應用已經有寫sd卡許可權,可以通過sd卡作為中轉。在測試用例執行完成的恰當時機,重新整理sd卡的coverage.ec。測試結束後匯出資料到build/outputs/code-coverage/connected/coverage.ec。最後執行jacoco的生成報告任務。
在測試用例執行完的恰當時機重新整理coverage.ec

,主要程式碼:

public class PushTestReport {

    public static String getSDPath(){
        File sdDir = null;
        boolean sdCardExist = Environment.getExternalStorageState()
                .equals(android.os.Environment.MEDIA_MOUNTED);
        if(sdCardExist)
        {
            sdDir = Environment.getExternalStorageDirectory();
        }
        return sdDir.toString();
    }


    public static void createReport() {

        String dir = getSDPath();
        String path = dir + "/coverage.ec";
        Log.d("jacoco","createReport:"+path);
        File file = new File(path);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
                Log.e("jacoco","pushReport,error:"+e.getMessage());
            }
        }

        OutputStream out = null;
        try {
            out = new FileOutputStream(path, false);
            Object agent = Class.forName("org.jacoco.agent.rt.RT")
                    .getMethod("getAgent")
                    .invoke(null);
            byte[] bytes = (byte[])agent.getClass().getMethod("getExecutionData", boolean.class)
                    .invoke(agent, false);

            Log.d("jacoco","pushReport,bytes:"+bytes.length);
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("jacoco","pushReport,error:"+e.getMessage());
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

呼叫時機tearDown():

 @After
    public void tearDown() throws Exception {
        PushTestReport.createReport();
    }

以及TestRule執行完用例處:

@Override
    public Statement apply(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                base.evaluate();
                PushTestReport.createReport();

            }
        };
    }

build.gradle如下:

task preJacocoTestReport(type: Exec) {
    executable 'sh'
    args "pre-jacoco-report.sh"
}


task jacocoTestReport(type: JacocoReport,dependsOn: preJacocoTestReport) {

    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled = true
        html.enabled = true
    }


    def coverageSourceDirs = [
            '../app/src/main/java'
    ]

    def excludeClasses = ['**/R*.class',
                          '**/*$InjectAdapter.class',
                          '**/*$ModuleAdapter.class',
                          '**/*$ViewInjector*.class',
                          '**/BuildConfig.class'
    ]

    classDirectories = fileTree(dir: '../app/build/intermediates/classes/debug',excludes: excludeClasses)
    
    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('$$', '$'))
            }
        }
    }
}

pre-jacoco-report.sh指令碼:

#!/usr/bin/env bash
#當前在環境為Project/app目錄
rm -f build/outputs/code-coverage/connected/coverage.ec
adb pull /storage/emulated/0/coverage.ec build/outputs/code-coverage/connected/

執行以下任務輸出報告:build/reports/jacoco/jacocoTestReport/html/index.html

gradle createDebugCoverageReport -p Project/app
gradle jacocoTestReport -p Project/app

這裡寫圖片描述

子模組覆蓋率統計注意

如果想要子模組的覆蓋率也統計進去

  1. 子模組的build.gradle中也增加testCoverageEnabled true

  2. src也要包含子模組的src,例如:

    def coverageSourceDirs = [
    ‘…/**/src/main/java’
    ]

  3. class也要包含子模組的class:

    classDirectories += fileTree(dir: ‘…/subMoudle1/build/intermediates/classes/debug’,excludes: excludeClasses)
    classDirectories += fileTree(dir: ‘…/subMoudle2/build/intermediates/classes/debug’,excludes: excludeClasses)

插曲

Jacoco一直報錯:

08-20 13:47:35.971 25869-25869/com.gs.service W/System.err: java.io.FileNotFoundException: /jacoco.exec (Read-only file system)
        at java.io.FileOutputStream.open(Native Method)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:221)
        at org.jacoco.agent.rt.internal_8ff85ea.output.FileOutput.openFile(FileOutput.java:67)
        at org.jacoco.agent.rt.internal_8ff85ea.output.FileOutput.startup(FileOutput.java:49)
08-20 13:47:35.972 25869-25869/com.gs.service W/System.err:     at org.jacoco.agent.rt.internal_8ff85ea.Agent.startup(Agent.java:122)
        at org.jacoco.agent.rt.internal_8ff85ea.Agent.getInstance(Agent.java:50)
        at org.jacoco.agent.rt.internal_8ff85ea.Offline.<clinit>(Offline.java:31)
        at org.jacoco.agent.rt.internal_8ff85ea.Offline.getProbes(Offline.java:51)

修改檔案許可權或者增加/jacoco-agent.properties 裡面destfile=/mnt/sdcard/jacoco.exec並不能解決問題。然而此異常也並不影響覆蓋率的統計。後續仍可深究。