JaCoCoの導入（UnitTest探求記6）

Unit Test 探求記は、unit test に関してまったりと実験しつつその過程を綴ってみるというものです。

# 今回のお題
- [JaCoCo](https://www.jacoco.org/jacoco/) plugin を導入する。
- Unit test と instrumented test の各々個別のレポートを生成する。
- Unit test と instrumented test を合成したレポートを生成する。

# JaCoCo とは
- [JaCoCo](https://www.jacoco.org/jacoco/) はコードカバレッジ用のライブラリでです。
- [The JaCoCo Plugin](https://docs.gradle.org/current/userguide/jacoco_plugin.html) でサポートされています。
- [Android Gradle plugin](https://developer.android.com/studio/releases/gradle-plugin) でもサポートされています。

# 前提
今回は以下のような gradle project 構成を仮定して進めます。

```
$ ./gradlew -q projects
Root project 'of-commons'
+--- Project ':android'
\--- Project ':kotlin'
```

- `:android` は android-library 用の plugin を使用している。
- `:kotlin` は kotlin 用の plugin を使用している。

# 今回のゴール
以下のタスクを作成することを今回のゴールとします。

|モジュール|レポートを生成するタスク             |レポートの対象となるテストタスク          |
|---------|-----------------------------------|---------------------------------------|
|:kotlin  |jacocoTestReport                   |test                                   |
|:android |jacocoTest<*Variant*>UnitTestReport|test<*Variant*>UnitTest                |
|         |jacocoTestReport                   |test                                   |
|         |jacocoConnectedAndroidTestReport   |connectedAndroidTest                   |
|         |jacocoMergeTestReport ※           |testDebugUnitTest, connectedAndroidTest|
※ jacocoMergeTestReport は、testDebugUnitTest と connectedAndroidTest の結果を合成した単一のレポートを生成する。

# Kotlin moduleへのJaCoCo導入

## ◆ コード
JaCoCo 関連の差分のみを以下に示します。

```kotlin:kotlin/build.gradle.kts
plugins {
    jacoco
}

// for JaCoCo
jacoco {
    toolVersion = JacocoUtils.toolVersion
}

// If you want to generate report always after tests run, please uncomment below.
// tasks.test {
//     // report is always generated after tests run
//     finalizedBy(tasks.jacocoTestReport)
// }

// If you want to run tests always before generating report, please uncomment below.
tasks.jacocoTestReport {
    // tests are required to run before generating the report
    dependsOn(tasks.test)
}
```

```kotlin:buildSrc/java/JacocoUtils.kt
object JacocoUtils {
    const val toolVersion: String = "0.8.5"
}
```

## ◆ 実行

### ☆ jacocoTestReport
```
$ ./gradlew :kotlin:jacocoTestReport
```
レポートは *kotlin/build/reports/jacoco/test/html/* 以下に出力されます。
![jacoco_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/fd15e312-3aad-9f44-ecab-ce5440f3c969.png)

# Android-library moduleへのJaCoCo導入

## ◆ コード
JaCoCo 関連の差分のみを以下に示します。

```kotlin:andrid/build.gradle.kts
plugins {
    jacoco
}

android {
    buildTypes {
        getByName("debug") {
            isTestCoverageEnabled = true
        }
    }
}

jacoco {
    toolVersion = JacocoUtils.toolVersion
}

afterEvaluate {
    // for 'jacocoTestXxxUnitTestReport' task (e.g. jacocoTestDebugUnitTestReport)
    android.libraryVariants.forEach { variant ->
        val variantName = variant.name
        val capitalizedVariantName = variantName.capitalize()
        createJacocoReportTask(
            "jacocoTest${capitalizedVariantName}UnitTestReport",
            variantName,
            "Generates code coverage report for the test${capitalizedVariantName}UnitTest task.",
            "test${capitalizedVariantName}UnitTest"
        )
    }

    // for 'jacocoTestReport' task
    task<JacocoReport>("jacocoTestReport") {
        android.libraryVariants.forEach { variant ->
            dependsOn("jacocoTest${variant.name.capitalize()}UnitTestReport")
        }
    }

    // for 'jacocoConnectedAndroidTestReport' task
    createJacocoReportTask(
        "jacocoConnectedAndroidTestReport",
        "debug",
        "Generates code coverage report for the connectedAndroidTest task.",
        "connectedAndroidTest"
    )

    // for 'jacocoMergeTestReport' task
    val testDebugUnitTest = "testDebugUnitTest"
    val connectedAndroidTest = "connectedAndroidTest"
    val description = "Generates code coverage report for the $testDebugUnitTest and $connectedAndroidTest tasks."
    createJacocoReportTask(
        "jacocoMergeTestReport",
        "debug",
        description,
        testDebugUnitTest,
        connectedAndroidTest
    )
}
```

以下は、タスク生成に関する共通ロジックを buildSrc 側に抜き出したものです。
build.gradle.kts 内部に記述しても問題はありません。

```buildSrc/src/main/java/JacocoUtils.kt
import org.gradle.api.Project
// import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.task
import org.gradle.testing.jacoco.tasks.JacocoReport

object JacocoUtils {
    const val toolVersion: String = "0.8.5"

    fun Project.createJacocoReportTask(reportTaskName: String, variantName: String, description: String, vararg testTaskNames: String) {
        task<JacocoReport>(reportTaskName) {
            testTaskNames.forEach { testTaskName ->
                dependsOn(testTaskName)

                // テストタスク完了時に必ずレポートタスクを実行させたい場合はアンコメントしてください。
                // report is always generated after tests run
                // tasks[testTaskName].finalizedBy(tasks[reportTaskName])
            }

            group = "verification"
            this.description = description

            // The following are the default settings for 'reports'
            // reports {
            //     csv.isEnabled = false
            //     html.isEnabled = true
            //     xml.isEnabled = false
            // }

            // Exclude the class files corresponding to the auto-generated source files.
            // TODO: 実際に目視確認したもののみを除外していく。(例えば R.class はまだ実際に目視確認してないので除外していない)
            val classDirectoriesTreeExcludes = setOf(
                // e.g. class   : androidx/databinding/library/baseAdapters/BR.class
                //      element : BR
                "androidx/**/*.class",

                // e.g. class   : <AndroidManifestPackage>/DataBinderMapperImpl.class
                //      element : DataBinderMapperImpl
                "**/DataBinderMapperImpl.class",

                // e.g. class   : <AndroidManifestPackage>/DataBinderMapperImpl$InnerBrLookup.class
                //      element : DataBinderMapperImpl.InnerBrLookup
                "**/DataBinderMapperImpl\$*.class",

                // e.g. class   : <AndroidManifestPackage>/BuildConfig.class
                //      element : BuildConfig
                "**/BuildConfig.class",

                // e.g. class   : <AndroidManifestPackage>/BR.class
                //      element : BR
                "**/BR.class",

                // e.g. class   : <AndroidManifestPackage>/DataBindingInfo.class
                //      element : DataBindingInfo
                "**/DataBindingInfo.class"

                //     "**/R.class",
                //     "**/R$*.class",
                //     "**/Manifest*.*",
                //     "android/**/*.*",
                //     "**/Lambda$*.class",
                //     "**/*\$Lambda$*.*",
                //     "**/Lambda.class",
                //     "**/*Lambda.class",
                //     "**/*Lambda*.class",
                //     "**/*Lambda*.*",
                //     "**/*Builder.*"
            )

            // classDirectories --------------------------------------------------------------------

            val javaClassDirectoriesTree = fileTree(
                mapOf(
                    "dir" to "${buildDir}/intermediates/javac/${variantName}/classes/",
                    "excludes" to classDirectoriesTreeExcludes
                )
            )

            val kotlinClassDirectoriesTree = fileTree(
                mapOf(
                    "dir" to "${buildDir}/tmp/kotlin-classes/${variantName}",
                    "excludes" to classDirectoriesTreeExcludes
                )
            )

            classDirectories.setFrom(files(javaClassDirectoriesTree, kotlinClassDirectoriesTree))

            // sourceDirectories -------------------------------------------------------------------

            val mainSourceDirectoryRelativePath = "src/main/java"
            val variantSourceDirectoryRelativePath = "src/${variantName}/java"
            sourceDirectories.setFrom(
                mainSourceDirectoryRelativePath,
                variantSourceDirectoryRelativePath
            )

            // executionData -----------------------------------------------------------------------

            executionData.setFrom(
                fileTree(
                    mapOf(
                        "dir" to project.projectDir,
                        "includes" to listOf(
                            // unit test execution data
                            "**/*.exec",
                            // instrumented test execution data
                            "**/*.ec"
                        )
                    )
                )
            )
        }
    }
}
```

## ◆ 実行

### ☆ jacocoTestDebugUnitTestReport
*testDebugUnitTest* タスクによるテストカバレッジをレポートします。

```
$ ./gradlew :android:jacocoTestDebugUnitTestReport
```
レポートは *android/build/reports/jacoco/jacocoTestDebugUnitTestReport/html/* 以下に出力されます。
![jacocoTestDebugUnitTestReport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/7bc63cf8-a981-eb5d-4298-229f135e358e.png)

### ☆ jacocoTestReleaseUnitTestReport
*testReleaseUnitTest* タスクによるテストカバレッジをレポートします。

```
$ ./gradlew :android:jacocoTestReleaseUnitTestReport
```
レポートは *android/build/reports/jacoco/jacocoTestReleaseUnitTestReport/html/* 以下に出力されます。
![jacocoTestReleaseUnitTestReport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/d5f1c63c-d6f0-deed-3f80-6be75d1cbc86.png)

### ☆ jacocoTestReport
*test* タスクによるテストカバレッジをレポートします。

```
$ ./gradlew :android:jacocoTestReport
```

test<*Variant*>UnitTest に当てはまるすべてのレポートが生成されます。
※ 上記2例と同様なのでレポート表示は割愛。

### ☆ jacocoConnectedAndroidTestReport
*connectedAndroidTest* タスクによるテストカバレッジをレポートします。

```
$ ./gradlew :android:jacocoConnectedAndroidTestReport
```
レポートは *android/build/reports/jacoco/jacocoConnectedAndroidTestReport/html/* 以下に出力されます。
![jacocoConnectedAndroidTestReport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/44d9914b-9524-4133-77c5-6f25153eda38.png)

### ☆ jacocoMergeTestReport
*testDebugUnitTest* タスクと *connectedAndroidTest* タスクによるテストカバレッジを統合してレポートします。

```
$ ./gradlew :android:jacocoMergeTestReport
```
レポートは *android/build/reports/jacoco/jacocoMergeTestReport/html/* 以下に出力されます。
![jacocoMergeTestReport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/e9bdc4fb-9557-af37-0940-97861411c8a3.png)

# 文字化け対策
レポートが文字化けする場合は、gradle.properties ファイルに以下の設定を追加してください。[^1]

```properties:gradle.properties
org.gradle.jvmargs=-Dfile.encoding=UTF-8
```

# まとめ
- [JaCoCo](https://www.jacoco.org/jacoco/) plugin を導入をしてみました。
- 今回 android についてはライブラリプロジェクトに対応しましたが、`android.libraryVariants` の個所を `android.applicationVariants` にするだけでアプリケーションプロジェクトにも対応できると思います。
- *classDirectoriesTreeExcludes* については今後の課題ということで。
- flavor については今後の課題ということで。
- GitHub のソースは[こちら](https://github.com/beyondseeker/of-commons/tree/4e3d5d03cc65b0eb6d31d0daff7ae78ce048ff5a/)。

[^1]: 裏は取ってないけど Windows 環境で発生するっぽいです。