横須賀第283管区情報局

2019/10/06

マルチモジュールなAndroidプロジェクトでJaCoCoの設定を書く

JVM言語のカバレッジレポートツール・JaCoCo。Androidの開発において、ユニットテスト環境の整備時に採用されることの多いライブラリですが、マルチモジュール構成 + Kotlin DSLなAndroidプロジェクトに導入した事例はまだまだ少ないように見えます。

そんな中、先日業務で関わるAndroidプロジェクトにJaCoCoを導入するタスクを消化したので、その時得た知見をネットの海に放流したいと思います。

#前提条件

  • バージョン
    • Kotlin: 1.3.50
    • JaCoCo: 0.8.4
    • Android Studio: 3.5.1
  • Kotlin DSLを利用

#シングルモジュールの場合

plugins { jacoco }でプラグインを適用すればOK。./gradle jacocoTestReportでレポートの出力ができます。

カバレッジレポートの形式やディレクトリの指定は、以下のスクリプトで設定可能です。

build.gradle.kts
plugins { jacoco } tasks.jacocoTestReport { reports { xml.isEnabled = false csv.isEnabled = false html.destination = file("${buildDir}/jacocoHtml") } }

その他細かい設定は、The JaCoCo Plugin | Gradle User Manualを参考に。

#Robolectric対応

Robolectricを利用したユニットテストは、includeNoLocationClasses = trueの設定追記が必要になります。Android + Kotlin DSLのビルドスクリプトならばこんな感じ。

build.gradle.kts
android { testOptions { unitTests.all(unitTests.closureOf<Test> { extensions.configure(JacocoTaskExtension::class.java) { isIncludeNoLocationClasses = true } } as Closure<Test>) } }

unitTests.all(...)辺りの書き方があんまりイケてないですが、よりシンプルに書ける方法は今のところなさそう🤔

(参考: [Android] - "android.testOptions" Groovy closure issue #440)

#マルチモジュールの場合

大きく分けて

  • モジュール毎にレポート出力タスクを実行
  • 全モジュールのカバレッジを1ファイルにマージし、レポート出力タスクを実行

の2通りの方法で、カバレッジレポートの出力を有効にすることができます。

Codecov等、レポートファイルの複数アップロードに対応しているサービスであれば前者の方法で問題ありません。

後者の方法は、カバレッジを単一のレポートファイルにまとめる必要がある場合に用います。また、モジュールの数が増えた場面でも、生成ファイルを1ヶ所にまとめることができるため、ファイルの取り回しがしやすくなる利点もあります。

という訳で、ここから先はカバレッジを1ファイルにマージする方法について、スクリプトの実装を解説していきます。

#大まかなタスクの流れ

実装するスクリプトでは、最終的に以下のようなタスクを順に実行します。

モジュール毎にユニットテストを実行

モジュール毎のカバレッジのマージ

1ファイルにまとめたカバレッジからレポート出力

新しく定義するタスクは「カバレッジのマージ」と「1ファイルにまとめたカバレッジからレポート出力」です。

#カバレッジのマージ

JaCoCoにはJacocoMergeというカバレッジマージ用のタスクが定義されているため、これをユニットテストの後に実行するよう、新しくタスクを定義します。

build.gradle.kts
tasks.create("jacocoMergeReports", JacocoMerge::class.java) { group = "reporting" description = "Merge all JaCoCo reports from projects into one." executionData = files() gradle.afterProject { if (rootProject == this || !plugins.hasPlugin("jacoco")) { return@afterProject } executionData += files("${buildDir}/jacoco/testDebugUnitTest.exec") dependsOn("testDebugUnitTest") } }

ポイントは以下2つ。

  • dependsOnメソッドで、「ユニットテストの実行後にカバレッジマージタスクを実行」というタスク間の依存関係を定義
  • setExecutionDataメソッドで、各モジュールのカバレッジファイルを指定
    • Kotlin DSLではexecutionData += files(...)と書くのがおそらく最もシンプル

#1ファイルにまとめたカバレッジからレポート出力

JacocoReportがレポートの出力タスクになります。このタスクを、カバレッジマージタスクの後に実行するよう、依存関係を定義します。

xml・html形式でレポートを出力する場合は、以下のようにタスクを定義します。

build.gradle.kts
val jacocoMergeReports = tasks.create("jacocoMergeReports", JacocoMerge::class.java) { ... } tasks.create("jacocoTestReports", JacocoReport::class.java) { group = "Reporting" description = "Generate Jacoco coverage reports for the build. Only unit tests." dependsOn(jacocoMergeReports) executionData.setFrom(jacocoMergeReports.destinationFile) var sourceDirs: FileCollection = files() subprojects.forEach { if (it.rootProject == this || !it.plugins.hasPlugin("jacoco")) { return@forEach } sourceDirs += files("${it.projectDir}/src/main/kotlin") } sourceDirectories.setFrom(sourceDirs) classDirectories.setFrom(fileTree( "dir" to ".", "includes" to listOf("**/tmp/kotlin-classes/debug/**"), "excludes" to listOf( // Android "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", "**/*Test*.*", "**/*Spec*.*", "android/**/*.*", "**/*Application.*", //Dagger 2 "**/*Dagger*Component*.*", "**/*Module.*", "**/*Module$*.*", "**/*MembersInjector*.*", "**/*_Factory*.*", "**/*Provide*Factory*.*" ) )) reports { xml.isEnabled = true html.isEnabled = true csv.isEnabled = false xml.destination = file("$buildDir/reports/jacoco/report.xml") html.destination = file("$buildDir/reports/jacoco/html") } }

ここでのポイントは以下3つ。

  • dependsOnメソッドで、「カバレッジマージの実行後にレポート出力を実行」というタスク間の依存関係を定義
  • setSourceDirectoriesメソッドで、カバレッジ対象のソースディレクトリを指定
  • setClassDirectoriesメソッドで、カバレッジ対象のクラスディレクトリを指定
    • Kotlinの場合、${buildDir}/tmp/kotlin-classes/${BuildVariants}
    • Javaの場合、${buildDir}/intermediates/classes/${BuildVariants}

これでスクリプト実装は終了です。./gradlew jacocoTestReportsを実行し、全てのモジュールでユニットテストが実行され、カバレッジレポートが生成されているか確認してみましょう。Robolectricを使うユニットテストが存在する場合、includeNoLocationClasses = trueの設定も忘れないように!

#Kotlin DSLで書く利点

初めて使うプロパティやメソッドの存在、および実装をIDEのタグジャンプで探せることが最大の利点だと、個人的には思います。Gradleの公式リファレンスにも必要な情報は存在しますが、「JaCoCoのGradleタスクにどんな設定値が存在するか」くらいの情報ならば、コードベースで探した方が圧倒的に早く得られる実感があります。

#まとめ

JaCoCoのスクリプト実装をKotlin DSLで書いた事例を記事としてまとめました。Kotlin DSL、慣れると作業効率が目に見えて上がるのですが、「とりあえず動作するスクリプトを1つ実装する」ことが難しく、なかなか一歩を踏み出せないという人が多い気がします。この記事がそのハードルを超える一助になれば…と思います。

#参考リンク