Java
JUnit
gradle
jacoco

Gradleのマルチプロジェクト構成でJUnitやJacocoのレポートを集計する

目的

Gradleでマルチプロジェクト構成の場合、testタスク実行時のテスト結果やJacocoのコードカバレッジは、通常サブプロジェクトごとに生成されます。
しかし、JenkinsなどのCIツールでテスト結果やカバレッジを集計したり、HTMLレポートの生成などを考えると、場合によってはプロジェクト全体の実行結果を集計する必要が出てきます。
ルートプロジェクト側にタスクを定義し、サブプロジェクトのテスト実行結果を集計したレポートを出力する方法をまとめます。

結論

下記の設定で、gradle check testReport jacocoReportと実行すればサブプロジェクトのJUnitレポート、Jacocoレポートの集計結果が出力されます。

build.gradle
subprojects {
    apply plugin: 'java'
    apply plugin: 'jacoco'

    tasks.withType(AbstractCompile).each { it.options.encoding = 'UTF-8' }

    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    // ...(Omitted)...
}

task testReport(type: TestReport) {
    reportOn subprojects.tasks.test.binResultsDir
    destinationDir file("$buildDir/reports/tests/test")
}

apply plugin: 'jacoco'

task jacocoMerge(type: JacocoMerge) {
    executionData project(':child1').tasks.test.jacoco.destinationFile
    executionData project(':child2').tasks.test.jacoco.destinationFile
}

task jacocoReport(type: JacocoReport) {
    dependsOn jacocoMerge

    executionData jacocoMerge.destinationFile
    sourceSets *subprojects.sourceSets.main
}

実行結果集計用のタスククラス

Gradleには、テスト実行結果、Jacocoカバレッジをそれぞれ集計するためのタスククラスがデフォルトで用意されています。

TestReport

TestReport - Gradle DSL Version 4.7
https://docs.gradle.org/4.7/dsl/org.gradle.api.tasks.testing.TestReport.html

テスト実行結果のHTMLレポート生成を行うためのタスククラスです。

task testReport(type: TestReport) {
    reportOn subprojects.tasks.test.binResultDir
    destinationDir file("$buildDir/reports/tests/test")
}

reportOnに指定したディレクトリからテスト実行結果を読み取って、destinationDirにHTMLレポートを出力します。gradleの出力系タスクにしては珍しく(?)デフォルト出力先が決まっていないので、destinationDirは設定必須です。

注意点

下記のように、reportOnにtestタスク自体を指定することも可能です。むしろこの書き方のほうがgradleらしく見えます。

reportOn subprojects.tasks.test

ただし、この場合は定義したタスクから各testタスクへの依存関係が設定されてしまいます。
このため、タスク実行時にサブプロジェクトすべてのtestタスクが実行され、さらにテストケースが失敗した場合にはタスクがスキップされてしまいます。

通常のtestタスク実行時は、テストケースの成否に関わらずHTMLレポートも出力されるはずなので、ディレクトリを指定したほうが想定通りの動きになるはずです。

参考URL: TestReport not generated if one of executed tests fails - Old Forum - Gradle Forums

JacocoMerge

JacocoMerge - Gradle DSL Version 4.7 https://docs.gradle.org/4.7/dsl/org.gradle.testing.jacoco.tasks.JacocoMerge.html

サブプロジェクトごとのカバレッジを集計するためのタスククラスです。

task jacocoMerge(type: JacocoMerge) {
    executionData project(':child1').tasks.test.jacoco.destinationFile
    executionData project(':child2').tasks.test.jacoco.destinationFile
}

executionDataに指定したjacocoの実行結果ファイルをマージした実行結果ファイルを出力します。デフォルトの出力先は${buildDir}/jacoco/${タスク名}.execです。

注意点

executionDataは可変長引数を受け取りますが、TestReportクラスのreportOnと違ってIterableを受け付けません。サブプロジェクトの指定を1行で済ませたい場合は、下記のようにGroovyのリスト展開演算子を使う必要があります。

executionData *subprojects.tasks.test.jacoco.destinationFile

ただし、jacocoMergeタスクはexecutionDataに存在しないファイルが指定されていると実行時にエラーとなります。サブプロジェクトすべてにテストがある場合は特に問題ありませんが、テストがないプロジェクトがある場合はexecutionDataに含まれないように設定する必要があります。

また、TestReportタスククラスのようにtestタスク自体を指定することも可能ですが、同様に依存関係が自動的に設定されてしまいます。これも避けたほうが良いでしょう。

executionData project(':child1').tasks.test

JacocoReport

JacocoReport - Gradle DSL Version 4.7 https://docs.gradle.org/4.7/dsl/org.gradle.testing.jacoco.tasks.JacocoReport.html

Jacocoの実行結果からレポートを生成するためのタスククラスです。
javaプラグインとjacocoプラグインの両方をapplyすると自動的に定義されるjacocoTestReportというタスクがありますが、これもJacocoReportタスククラスを元に定義されています。

task jacocoReport(type: JacocoReport) {
    executionData jacocoMerge.destinationFile
    sourceSets *subprojects.sourceSets.main
}

executionDataで、jacocoMergeタスクで生成した集計済みの実行結果ファイルを指定します。sourceSetsは何故かドキュメントに載っていませんが、ソースセットを指定するだけでsourceDirectoriesclassDirecrtoriesの両方を設定してくれます。

参考URL: gradle/JacocoReportBase.java at master · gradle/gradle

まとめ

Gradleのサブプロジェクトのレポート集計については、あまり利用シーンが少ないのか、細かい部分でAPIがイマイチな印象を受けます。特に依存関係周りはうまくいっていると気付きにくいので注意が必要ですね。

参考URL

GradleでマルチプロジェクトのJUnit ReportとJacoco Reportを出す - clash_m45の開発ブログ
Gradleのマルチプロジェクトのテスト結果とカバレッジをそれぞれ1つにまとめる - Qiita