ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Gradle のマルチモジュールプロジェクトで JaCoCo の結果を集計する

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

f:id:nobuoka:20180814004742p:plain

(この図は 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 Test are automatically enhanced to provide coverage information when the java plugin has been applied, any task that implements JavaForkOptions can be enhanced by the JaCoCo plugin. That is, any task that forks Java processes can be used to generate coverage information.

The JaCoCo Plugin - Gradle User Manual

バージョンを表す変数の定義や 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 が期待するレポートファイルの位置である。

サンプル

下記プロジェクトで今回書いたコードカバレッジ取得の方法を行っている。

ちなみに CircleCI でのビルド時にコードカバレッジを取得して Codecov で可視化してる。

参考