Java / Kotlin のコードカバレッジツールとして JaCoCo を使いたい。 Gradle のマルチモジュールプロジェクトでの JaCoCo の導入について記す。

(この図は JaCoCo によるコードカバレッジの集計結果の履歴を Codecov で表示した例。)
JaCoCo について
JaCoCo は、JVM 言語のコードカバレッジツールである。 Java Agent によるオンザフライ方式のバイトコード instrumentation によるカバレッジ計測が可能である。
以前、Kotlin のコードカバレッジツールについて書いたので、こちらも参考にどうぞ。
Gradle プロジェクトで JaCoCo を使ったカバレッジ計測を行う
Gradle の JaCoCo プラグインを使うことで、Gradle のプロジェクトで JaCoCo を使ったコードカバレッジ計測を手軽に行うことができる。 ちなみに Gradle 4.9 の段階ではこのプラグインは incubating である。
Junit 5 のテスト実行時にカバレッジを計測する
各モジュールのテスト実行時にコードカバレッジを計測するだけなら非常に簡単で、単に JaCoCo プラグインを適用するだけで良い。
While all tasks of type
The JaCoCo Plugin - Gradle User ManualTestare automatically enhanced to provide coverage information when thejavaplugin has been applied, any task that implementsJavaForkOptionscan be enhanced by the JaCoCo plugin. That is, any task that forks Java processes can be used to generate coverage information.
バージョンを表す変数の定義や repositories の定義などは省略しているが、おおよそ以下のようなビルドスクリプトで JUnit 5 でのテスト実行時に JaCoCo によるコードカバレッジの計測がなされる。
apply plugin: 'kotlin' apply plugin: 'jacoco' test { useJUnitPlatform() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompileOnly("org.junit.jupiter:junit-jupiter-api:$junit_version") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junit_version") }
デフォルトでは、カバレッジの計測結果は build/jacoco/test.exec というファイルに出力されるようである。
複数モジュールのコードカバレッジの集計
難しいのはここからで、複数モジュールでのコードカバレッジをどう集計するかで私は結構悩んだ。 JaCoCo の結果集計のためのタスクとしては JacocoMerge というのがあるのだが、これをどう定義するのかが難しい。 (どのサブモジュールのカバレッジを集計するのかを明示的に指定するのであればそれほど大変ではないが、自動で全サブモジュールのカバレッジを集計するためのタスク定義をするのが難しい。)
結論としては、ルートプロジェクトに以下のようなタスク定義を行うことで対応した。 (ルートプロジェクトにも jacoco プラグインを適用している。)
task jacocoMerge(type: JacocoMerge) {
// ルートプロジェクトの評価時にはまだサブプロジェクトは存在しないので、
// 各プロジェクトの評価完了時に中の処理が走るように。
gradle.afterProject { p, state ->
// ルートプロジェクトと `jacoco` プラグインが適用されていないプロジェクトは除く。
if (p.rootProject != p && p.plugins.hasPlugin('jacoco')) {
executionData p.tasks.test.jacoco.destinationFile
dependsOn(p.tasks.test)
}
}
}
大雑把に説明すると、jacocoMerge タスクの executionData に各サブモジュールのテスト実行時のカバレッジ計測結果を渡していき、かつ、jacocoMerge タスクの依存に各サブモジュールの test タスクを追加する、ということをしている。
gradle.afterProject を使っている理由や if 文を使っている理由はコメントに書いてあるとおりである。
Gradle のプロジェクトの評価順については下記ページを参照されたい。
集計結果のコードカバレッジのレポーティング
さらにレポーティングのタスクは、ルートプロジェクトに下記のように定義することで行える。 java プラグインが適用されている全サブモジュールのソースファイルをコードカバレッジのレポート対象に含める場合の例である。
task jacocoMergedReport(type: JacocoReport, dependsOn: [tasks.jacocoMerge]) {
executionData jacocoMerge.destinationFile
sourceDirectories = files()
classDirectories = files()
gradle.afterProject { p, state ->
if (p.rootProject != p && p.plugins.hasPlugin('java')) {
sourceDirectories = sourceDirectories + files([p.sourceSets.main.allJava.srcDirs])
classDirectories = classDirectories + p.sourceSets.main.output
}
}
reports {
xml.enabled = true
xml.destination file("${buildDir}/reports/jacoco/report.xml")
html.destination file("${buildDir}/reports/jacoco/html")
}
}
本当は JacocoReport#sourceSets を使いたかったが、内部で gradle.afterProject を使われていて期待する挙動にならなかったので諦めた。
ちなみに上の xml.destination の値は Codecov が期待するレポートファイルの位置である。
参考
- Gradleのマルチプロジェクト構成でJUnitやJacocoのレポートを集計する : プロジェクトの評価順を気にしなくてよければ
gradle.afterProjectを使わずにこういう感じで書ける。 - Aggregated Jacoco reports in a multi-project Gradle build · GitHub : こちらもプロジェクトの評価順を気にしなくてよいように書いた場合の例。