1. ホーム
  2. テンソルフロー

Android + jacocoでコードカバレッジを実現する最も正しい方法、bar none!

2022-02-24 12:19:51
<パス <ブロッククオート

まえがき : jacocoはJava Code Coverageの略で、Javaのコードカバレッジを統計する代表的なツールの1つです。jacocoの原理については、Web上に多くの記事があるので、興味のある方は他のブログを探してみてください、ここでは詳細は割愛します。その役割は、Androidプロジェクトのコードカバレッジ統計は、jacocoのオフライン杭の挿入方法を使用して、ファイルの最初の山の前にテストで、次に挿入された杭クラスやjarパッケージを生成し、テスト(ユニットテスト、UIテストやマニュアルテストなど)挿入杭クラスとjarパッケージ、ファイルに動的カバレッジ情報を生成し、最後に統一カバレッジ情報を処理し、レポートを生成することである。

この要件を受けたとき、私は開発者が提出したコードのセルフテスト率をカウントする必要があり、他のソースとgradleの勧告から学んだ方法を実装するためにjacoco、その後、また多くの情報をグーグル、オンライン情報は非常に古いですgradleプラグインの依存関係が1.+または2.+、Gradle依存度はまだ4程度である。 4、だからそれは私の時間の多くを無駄にする問題につながった:インターネット上の情報は時代に追いついていないが、私は最終的に突破口を見つけ、多くのハードワークの後に達成した後、この分野で主張する学生に方向を与えるために最新の、正しいjacoco + Android統合練習ブログ記事はありません、明るい光を点灯する学生の将来のニーズのために、この問題を記録することにしました!それは非常に重要な問題で、そのような問題で、それは私の時間を浪費することにつながる、それは非常に重要な問題である、私は、この問題に対処するために、そのような問題ではないことを確認する必要があります。

まず、私の環境は、現在主流となっているプロジェクト開発環境でもあるはずですが、比較的新しいものです。

1. gradle plugin version.
	classpath 'com.android.tools.build:gradle:3.5.1' (root build.gradle)
	
2. gradle dependency version.
	distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip (gradle-wrapper.properties)
	
3. android sdk version.
	minSdkVersion 19
	targetSdkVersion 28 (app module build.gradle)


既存のブログ記事の問題点をネットで話し、実装コードのフルバージョンは記事の最後に添付する予定です。

I. レコードを踏む

1. classDirectoriesのパスが正しくない。

例として、オンラインコードは以下のように記述されています。

classDirectories = fileTree(dir: ". /build/intermediates/classes/debug", excludes: excludesFilter)


まず、このパスはgradleのバージョンが古い場合、コンパイルを実行するとこのパスの下にクラスファイルが生成されるのですが、私が使っている開発環境の基準ではapp/build/intermediates/classesは全く内容がなく、この問題で長い間止まってしまい、しばらくは何か設定が間違っていて私のプロジェクトは正しくクラスファイルが生成されていないと思い、落ち込んでいたのです

しかし、gradleのバージョンの違いだとわかってからは、新しいバージョンのgradleでは、ソースコードをコンパイルする際に、全く正しいパスではないパスが生成され、それが

app/build/intermediates/javac/debug/classes


すべてクリア!!! このディレクトリの中に、クラスファイルに必要なものがすべてありました。

2. 複数モジュールの依存性カバレッジ統計

これが不満な点の2つ目です。ウェブ上のすべてのブログ記事に目を通しましたが、複数のライブラリの依存関係のカバレッジ統計に関わるものは、すべて次のような方法で実装されています。
moduleA の元の依存性メソッドをから変更します。

compile project(':moduleB')


になります。

debugCompile project(path:':moduleB',configuration:'debug')


デバッグパッケージを提供する方法でlirbrayプロジェクトをコンパイルさせる方法も調べましたが、configurationを使うとコンパイルがどうしても通りません、お兄さん、コンパイル依存は何年前だよ、今は実装やapiに置き換わっているよ? configurationはもう存在しないから、このモジュール依存のやり方は使えないよ!」と言われました。

カバレッジ統計に依存するモジュールに対応するbuild.gradleの数カ所を追加するだけで、以下のコードでjacoco-config.gradleファイルを構成することができるようになります。

apply plugin: 'jacoco'
android {
  defaultPublishConfig "debug"
  buildTypes {
    debug {
      /* Turn on coverage statistics switch**/
      testCoverageEnabled = true
    }
  }
}


カバレッジを計算する必要があるMODのbuild.gradleにあるこのgradleファイルを頼りにしてください。
説明の一つです。

defaultPublishConfig "debug"


これは、私たちのモジュールがデフォルトでデバッグを公開することを説明するものです。

api project(path: ':app_jinggong_sdk')


これだけです。余計なディレクティブを追加する必要はなく、これだけで十分です。

3. 実行データファイルが読めない .../coverage.ec

一部の人々は、生成されたecファイルの実行に依存してこの問題をスローします、ecを処理する権限がない、または失敗を読んで、あまり考えていない、最後にjacocoのバージョンを調整し、これを使用する、良い!。

jacoco {
  toolVersion = "0.8.2"
}


4. ecファイルの保存先

インターネットでは、このパスを使用するように記述されています。

"/mnt/sdcard/coverage.ec"


皆さんはどうかわかりませんが、私はこのパスで直接拒否され、保存時に「このディレクトリに書き込む権限がない」とエラーが出ました、悲しいです そこで、パスを変更して、この一行のコードでファイルをfilesディレクトリに保存してみましょう。

getContext().getFilesDir().getPath() + "/coverage.ec"


個人的に良いテスト、別の点がある、我々は何度もecファイルの生成をテストするとき、私はディレクトリcoverage.ec作成時間をファイルを参照してくださいにAndroid Studioのデバイスファイルエクスプローラを通じて、最後の時間をされている、最初は私はそれがキャッシュの問題だと思った、ブラウザが更新する時間を持っていなかったし、偶然、私はナイーブだった あなたもこの状況が発生した場合、あなたの携帯電話を抜いて、それに接続し、その後見て、すぐにファイルを生成する最後の操作ではないリフレッシュされていません!私は、あなたの携帯電話、そしてそれを見て、あなたの携帯電話に接続し、その後に、私はあなたがこのような状況に遭遇した場合は、あなたの携帯電話に接続する必要があります!私はあなたの携帯電話に接続する必要があります。

5. 実行データパスエラー

Web上のブログでは、このように書かれています。

executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")


実はこの意味を理解する必要があって、executionDataはjacocoがパースしたecファイルを実行するディレクトリを指しているので、この書き方を直すのではなく、プロジェクトの実行タスクcreateDebugCoverageReportが生成するディレクトリをメインとして使用すればいいんです、わかりますか?例えば、私の環境でcreateDebugCoverageReportコマンドを実行した後、coverage.ecファイルが生成するパスは以下のように表示されます。

だから、私のパスは

executionData = files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec")


つまり、この意味を理解すれば、サンプルに適切な変更を加えることができるのです。

II. 練習は完璧にする

以上、いくつかの落とし穴を挙げてみましたが、今まで悩んでいた学生さんは、自分の問題がどこにあるのかを知ることができるはずです。ここでは、実装コードのフルバージョンは、ここで直接マルチモジュールの例に、あなたは唯一のモジュールを適用している場合は、その上に対応するコードを修正し、これも同じことが二度話すことを避けることができます。

これらのファイルや他のオンライン同じの次のリストは、あなたが直接使用を引き継ぐことができ、ここで実際に私たちのメインアクティビティを聞くために使用され、これは一般的に我々のアプリMainActivityのホームページですが、活動を開始すると、それを解釈しないでくださいは1つのことです。このアクティビティは、onDestroyメソッドを実行するときにecファイルを生成するためにInstrumentationを通知するので、このアイデアによると行きたいとは思わないが、全く問題ない、ツールクラスを達成するために、ecファイルの生成を実行したいときに呼び出すことができます同じ理由、シナリオやニーズの使用に応じて、これ以上ナンセンスです。

1. FinishListener

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

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

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, "Exception : " + e);
        e.printStackTrace();
      }
    }
    if (arguments ! = null) {
      LogUtil.e(TAG, "arguments is not null : " + 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 {
        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);
    }
  }
}



2. InstrumentedActivity

apply plugin: 'jacoco'

jacoco {
  toolVersion = "0.8.2"
}
// Source paths, you write as many paths as you have mods here
def coverageSourceDirs = [
    '... /app/src/main/java',
    '... /app_jinggong_sdk/src/main/java',
    '... /app_jinggong_store/src/main/java',
    '... /app_jinggong_flutter/src/main/java',
    '... /app_jinggong_libcore/src/main/java',
]

//class file path, is the class path I mentioned above, see what your project class generation path is, replace mine on the line
def coverageClassDirs = [
    '... /app/build/intermediates/javac/debug/classes',
    '... /app_jinggong_sdk/build/intermediates/javac/debug/classes',
    '... /app_jinggong_store/build/intermediates/javac/debug/classes',
    '... /app_jinggong_flutter/build/intermediates/javac/debug/classes',
    '... /app_jinggong_libcore/build/intermediates/javac/debug/classes',
]

// This is the task that parses the ec file, and will parse the output according to the class path, source path, and ec path we specified
task jacocoTestReport(type: JacocoReport) {
  group = "Reporting"
  description = "Generate Jacoco coverage reports after running tests."
  reports {
    xml.enabled = true
    html.enabled = true
  html.enabled = true }
  classDirectories = files(files(coverageClassDirs).files.collect {
    fileTree(dir: it,
        // Filter the class files that don't need statistics
        excludes: ['**/R*.class',
            '**/*$InjectAdapter.class',
            '**/*$ModuleAdapter.class',
            '**/*$ViewInjector*.class'
        ])
  })
  sourceDirectories = files(coverSourceDirs)
  executionData = files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec")

  doFirst {
  	// iterate through all files under the class path, replacing characters
    coverageClassDirs.each { path ->
      new File(path).eachFileRecurse { file ->
        if (file.name.contains('$$')) {
          file.renameTo(file.path.replace('$$', '$'))
        }
      }
    }
  }
}


3. ジャココインストゥルメント

apply plugin: 'com.android.application'
apply from: 'jacoco.gradle'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.zhangyan.test"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1200000
        versionName "2.0.0"
        ndk {
            abiFilters "armeabi" //Temporary support for emulators, remove x86 before going live
        }
        multiDexEnabled true
    }
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true //Obfuscate switch to remove obfuscation when you encounter an online class that you can't find.
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            /* Turn on coverage statistics switch */
            testCoverageEnabled = true
            signingConfig signingConfigs.BeikeConfig
            debuggable true
            shrinkResources false
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // main business SDK
    api project(path: ':app_jinggong_sdk')
}


実は、この中には最適化できるコードがたくさんあるのですが、ここでは深く調査していません。

4. appモジュールの下にjacoco.gradleファイルを新規に作成します。

アプリモジュール build.gradle に提供されるこの jacoco.gradle ファイルは、次のコードに示すように、jacoco プラグインに依存し、jacoco のバージョン番号を指定し、レポートを生成するタスクを作成する役割を担っています。

// main business sdk
api project(path: ':app_jinggong_sdk')


そして、アプリのbuild.gradleファイルでこのjacoco.gradleに依存します。以下に一般的な例をあげます。

apply plugin: 'com.android.library'
apply from: '... /gradleCommon/jacoco-config.gradle'


5. 依存ライブラリモジュールに依存関係を追加する

アプリのbuild.gradleで、business modに依存していることを確認してください。

// Add the required permissions
<uss-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" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
    android:name="com.zhangyan.test.MyApplication"
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:largeHeap="true"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">

  ... Omit the rest of your own configuration and pages...
  
  <activity
    android:name=".jacoco.InstrumentedActivity"
    android:label="InstrumentationActivity" />
</application>

<Instrumentation
  android:name=".jacoco.JacocoInstrumentation"
  android:handleProfiling="true"
  android:label="CoverageInstrumentation"
  android:targetPackage="com.zhangyan.test" />



サブモジュールのコードカバレッジを計算する必要がある場合、サブモジュールを適宜修正する必要があります。具体的な jacoco-config.gradle については、上記のセクション 1、セクション 2 を参照してください。

この時点で、私の app_jinggong_sdk モジュールの build.gradle ファイルを開き、コードを追加します。

adb shell am instrument com.zhangyan.test/com.zhangyan.test.jacoco.JacocoInstrumentation


具体的な依存関係はすべてjacoco-config.gradleにあるので、モジュールプロジェクトもコードをカウントするスイッチが入り、コードカバレッジの統計ができるようになります。もし、依存するサブモジュールがいくつあっても、それぞれのサブモジュールの build.gradle ファイルに jacoco-config.gradle の依存関係を追加すればよいのです。

6. AndroidManifest.xml を設定する

appモジュールのAndroidManifest.xmlに設定を追加して、上で追加したInstrumentedActivityとJacocoInstrumentationを設定します。

app/build/reports/jacoco/jacocoTestReport/html/index.html


設定後、赤字で targetPackage="com.zhangyan.test" とありますが、大丈夫です、targetPackage はアプリケーションのパッケージ名と対応しているので、気にしないでください。

7. テストレポートを作成する

1. installDebug

まず、コマンドラインからアプリをインストールします。

アプリを選択 -> タスク -> インストール -> installDebugで、アプリを携帯電話にインストールします。

2. コマンドラインでの起動

adb shell am instrument com.zhangyan.test/com.zhangyan.test.jacoco.JacocoInstrumentation


上記のコマンドライン、adb shell am instrument appパッケージ名/Instrumentationフルパス名でアプリを起動します。

3. クリックでテスト

この時点で、アプリを操作し、コードカバレッジテストを行いたいページに移動し、対応するボタンをクリックし、対応するロジックを起動すれば、今行っていることが記録され、生成された coverage.ec ファイルに反映されることになります。クリックし終わったら、先ほど設定したロジックに従って、MainActivityのonDestroyメソッドを実行すると、JacocoInstrumentationにcoverage.ecファイルが生成されるので、returnボタンを押してMainActivityを終了してデスクトップに戻れば、カバレッジ.ecファイルの生成には少し時間がかかります(テストページのクリック数によっては、テスト数が多く、生成するファイルが大きくなると、時間がかかっても大丈夫な場合があります)。

次に、Android StudioのDevice File Exploreで、data/data/package/files/coverage.ecファイルを見つけて、右クリックでデスクトップに保存します(お好みでどうぞ)。

4.createDebugCoverageReport(デバッグカバレッジレポート)の作成

このコマンドが存在する通常のパスは

ダブルクリックすると、カバレッジレポートを作成するためのコマンドが実行されます。

完了すると、このパスの下にcoverage.ecファイルが作成されるので、考えずに削除してください そして、デスクトップにあるcoverage.ecファイルをこのパスにコピーします(もちろん、coverage.ecファイルをコピーするパスは変更可能で、jacoco.gradleの実行データに対応するパスも変更する必要があります)。

5. jacocoTestReport(ジャココテストレポート


このパスを見つけ、ダブルクリックしてこのタスクを実行すると、最終的に必要となるコードカバレッジレポートが生成され、実行後、このディレクトリに見つけることができます。

app/build/reports/jacoco/jacocoTestReport/html/index.html


フォルダをダブルクリックして開くと、コードカバレッジレポートが表示されます。

8. レポートの分析

私のプロジェクトの実際の結果を例にとると、index.htmlを開いた後、最初に表示されるのは、すべてのディレクトリの全体的なカバレッジです。

をクリックしてください。

ページの結果を見てみましょう

緑は実行されたコード、赤は実行されなかったコードです。これを元にテストロジックを改良し、コミット前にできるだけ多くのコードカバレッジを達成し、テストされなかったロジックを見逃さないようにします。

さて、ここでAndroid + jacocoの完全な統合が終了しました、今日それを使用したい学生のための指針になるはずです、何か質問はコメントを残してください