AndroidStudio 最新 jacoco實現(xiàn)代碼覆蓋率測試

一、前言

JaCoCo 是一個免費的 Java 代碼覆蓋率庫,由 EclEmma 團(tuán)隊根據(jù)多年使用和集成現(xiàn)有庫的經(jīng)驗教訓(xùn)創(chuàng)建。官網(wǎng)地址

JaCoCo是面向Java的開源代碼覆蓋率工具,JaCoCo以Java代理模式運行,它負(fù)責(zé)在運行測試時檢測字節(jié)碼。 JaCoCo會深入研究每個指令,并顯示每個測試過程中要執(zhí)行的行。 為了收集覆蓋率數(shù)據(jù),JaCoCo使用ASM即時進(jìn)行代碼檢測,并在此過程中從JVM Tool Interface接收事件,最終生成代碼覆蓋率報告。

Jacoco運行有離線(offline)、在線(on the fly)模式之說,所謂在線模式就是在應(yīng)用啟動時加入jacoco agent進(jìn)行插樁,在開發(fā)、測試人員使用應(yīng)用期間實時地進(jìn)行代碼覆蓋率分析。相信很多的java項目開發(fā)人員并不會去寫單元測試代碼的,因此覆蓋率統(tǒng)計就要把手工測試或接口測試覆蓋的情況作為重要依據(jù),顯然在線模式更符合實際需求。

二、AndroidStudio 集成Jacoco

  1. 環(huán)境
  • 根目錄build.gradle
buildscript {
    dependencies {
        classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3'
        classpath "org.jacoco:org.jacoco.core:0.8.8"
    }
}
plugins {
    id 'com.android.application' version '7.3.0' apply false
    id 'com.android.library' version '7.3.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
    id 'com.google.dagger.hilt.android' version '2.44' apply false
}

  • gradle依賴版本
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
  • 編譯sdk

compileSdk : 33
minSdk : 21
targetSdk : 33

3、 配置jacoco

  1. 在項目根目錄新建jacoco.gradle 文件
apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.8.8"
}

tasks.withType(Test) {
    jacoco.includeNoLocationClasses = true
}

ext {
    getFileFilter = { ->
        def jacocoSkipClasses = null
        if (project.hasProperty('jacocoSkipClasses')) {
            jacocoSkipClasses = project.property('jacocoSkipClasses')
        }
        //忽略類文件配置
        def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*$ViewInjector*.*']
        if (jacocoSkipClasses != null) {
            fileFilter.addAll(jacocoSkipClasses)
        }
        return fileFilter
    }
}

task jacocoTestReport(type: JacocoReport) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports"
    reports {
        xml.enabled(true)
        html.enabled(true)
    }

    def fileFilter = project.getFileFilter()
    println("files:"+fileFilter)
    def coverageClassDirs = ["$project.buildDir/intermediates/javac/debug/classes"
                             , "$rootDir/module_data/build/intermediates/javac/debug/classes"]

    getClassDirectories().setFrom(files(files(coverageClassDirs).files.collect {
        println("class:"+it)
        fileTree(dir: it, excludes: fileFilter)
    }))
    def coverageSourceDirs = ["$project.projectDir/src/main/java", "$rootDir/module_data/src/main/java"]
    //設(shè)置需要檢測覆蓋率的目錄
    println("source:"+coverageSourceDirs)
    getSourceDirectories().setFrom(files(coverageSourceDirs))

   def executionDirs=["$project.projectDir/build/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"]
    println("exe:"+executionDirs)
    //以下路徑也需要檢查
//    getExecutionData().setFrom(fileTree(dir: project.buildDir, includes: ['outputs/code-coverage/debugAndroidTest/connected/coverage.ec']))
    getExecutionData().setFrom(files(executionDirs))
    doFirst {
        //遍歷class路徑下的所有文件,替換字符
        coverageClassDirs.each { path ->
            new File(path).eachFileRecurse { file ->
                if (file.name.contains('$$')) {
                    file.renameTo(file.path.replace('$$', '$'))
                }
            }
        }
    }
}
  1. 生成ec 文件。
    在app/module 包下新建test目錄,新建如下類
    FinishListener
public interface FinishListener {
  void onActivityFinished();
  void dumpIntermediateCoverage(String filePath);
}

InstrumentedActivity

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

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) {
    LogUtil.e(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.isFile() && file.exists()) {
      if (file.delete()) {
        LogUtil.e(TAG, "file del successs");
      } else {
        LogUtil.e(TAG, "file del fail !");
      }
    }
    if (!file.exists()) {
      try {
        file.createNewFile();
      } catch (IOException e) {
        LogUtil.e(TAG, "異常 : " + e);
        e.printStackTrace();
      }
    }
    if (arguments != null) {
      LogUtil.e(TAG, "arguments不為空 : " + arguments);
      mCoverageFilePath = arguments.getString("coverageFile");
      LogUtil.e(TAG, "mCoverageFilePath = " + mCoverageFilePath);
    }

    mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
    mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    start();
  }

  @Override
  public void onStart() {
    LogUtil.e(TAG, "onStart def");
    if (LOGD) {
      LogUtil.e(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) {
      LogUtil.e(TAG, e.toString());
      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;
    LogUtil.e(TAG, msg);
    mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
        + msg);
  }

  @Override
  public void onActivityFinished() {
    if (LOGD) {
      LogUtil.e(TAG, "onActivityFinished()");
    }
    if (mCoverage) {
      LogUtil.e(TAG, "onActivityFinished mCoverage true");
      generateCoverageReport();
    }
    finish(Activity.RESULT_OK, mResults);
  }

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

配置AndroidManifest.xml

  <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
    <activity
            android:name=".test.InstrumentedActivity"
            android:label="InstrumentationActivity" />
     


    </application>
    <instrumentation
        android:name=".test.JacocoInstrumentation"
        android:handleProfiling="true"
        android:label="CoverageInstrumentation"
        android:targetPackage="com.qianyin.user" />

三、生成測試報告

  1. installDebug
    我們通過命令行安裝app,選擇你的app -> Tasks -> install -> installDebug,安裝app到你的設(shè)備上。

  2. 命令行啟動app
    先列出手機中已安裝的instrumentation:

adb shell pm list instrumentation

啟動app

adb shell am instrument com.qianyin.user/.test.JacocoInstrumentation

  1. 正常點擊測試app的功能
    這個時候你可以操作你的app,對你想進(jìn)行代碼覆蓋率檢測的地方,進(jìn)入到對應(yīng)的頁面,點擊對應(yīng)的按鈕,觸發(fā)對應(yīng)的邏輯,你現(xiàn)在所操作的都會被記錄下來,在生成的coverage.ec文件中都能體現(xiàn)出來。當(dāng)你點擊完了,根據(jù)我們之前設(shè)置的邏輯,當(dāng)我們MainActivity執(zhí)行onDestroy方法時才會通知JacocoInstrumentation生成coverage.ec文件,我們可以按返回鍵退出MainActivity返回桌面,生成coverage.ec文件可能需要一點時間哦(取決于你點擊測試頁面多少,測試越多,生成文件越大,所需時間可能多一點)

然后在Android Studio的Device File Explore中,找到data/data/包名/files/coverage.ec文件,右鍵保存到桌面?zhèn)溆?/p>

  1. createDebugCoverageReport


    image.png

    雙擊它,會執(zhí)行創(chuàng)建覆蓋率報告的命令,等待它執(zhí)行完,這個會生成一個coverage.ec文件,但是這個不是我們最終需要分析的,我們需要分析的是我們剛才手動點擊保存到桌面的那個。將保存到桌面的ec文件覆蓋這個ec文件。


    image.png

備注
jacoco.gradle 文件中g(shù)etExecutionData().setFrom(files(executionDirs)) 中 文件目錄要和coverage.ec文件的目錄保持一致否則會有問題。

  1. jacocoTestReport


    image.png

    找到這個路徑,雙擊執(zhí)行這個任務(wù),會生成我們最終所需要代碼覆蓋率報告,執(zhí)行完后,我們可以在這個目錄下找到它。生成文件的目錄可以自定義

app/build/reports/jacoco/jacocoTestReport/html/index.html
  1. 分析報告


    image.png

四、問題

1、執(zhí)行 jacocoTestReport task 出現(xiàn) Task :app:jacocoTestReport SKIPPED
可能原因:在 gradle 配置的路徑,比如上面的 app/build/outputs/code-coverage/connected/ 路徑創(chuàng)建失敗或有錯誤;檢查路徑是否有拼寫錯誤,是否與配置的一致。該路徑在配置時可自定義,在定義后存放 .ec 文件的路徑需與此保持一致!
2、gradle 的更新,一些gradle方法已經(jīng)過時,建議更新成最新方法使用,如getClassDirectories();getSourceDirectories();getExecutionData()

五、參考

Android jacoco
Android+jacoco實現(xiàn)代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容