5
8

More than 3 years have passed since last update.

JaCoCoの導入(UnitTest探求記6)

Last updated at Posted at 2020-08-30

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

今回のお題

  • JaCoCo plugin を導入する。
  • Unit test と instrumented test の各々個別のレポートを生成する。
  • Unit test と instrumented test を合成したレポートを生成する。

JaCoCo とは

前提

今回は以下のような 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/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)
}
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

Android-library moduleへのJaCoCo導入

◆ コード

JaCoCo 関連の差分のみを以下に示します。

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

☆ jacocoTestReleaseUnitTestReport

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

$ ./gradlew :android:jacocoTestReleaseUnitTestReport

レポートは android/build/reports/jacoco/jacocoTestReleaseUnitTestReport/html/ 以下に出力されます。
jacocoTestReleaseUnitTestReport.png

☆ jacocoTestReport

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

$ ./gradlew :android:jacocoTestReport

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

☆ jacocoConnectedAndroidTestReport

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

$ ./gradlew :android:jacocoConnectedAndroidTestReport

レポートは android/build/reports/jacoco/jacocoConnectedAndroidTestReport/html/ 以下に出力されます。
jacocoConnectedAndroidTestReport.png

☆ jacocoMergeTestReport

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

$ ./gradlew :android:jacocoMergeTestReport

レポートは android/build/reports/jacoco/jacocoMergeTestReport/html/ 以下に出力されます。
jacocoMergeTestReport.png

文字化け対策

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

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

まとめ

  • JaCoCo plugin を導入をしてみました。
  • 今回 android についてはライブラリプロジェクトに対応しましたが、android.libraryVariants の個所を android.applicationVariants にするだけでアプリケーションプロジェクトにも対応できると思います。
  • classDirectoriesTreeExcludes については今後の課題ということで。
  • flavor については今後の課題ということで。
  • GitHub のソースはこちら

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

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8