Androidでコードカバレッジを計測する

こんにちは。Yenom Androidエンジニアのゆいき(@yuikimil)です。このYenom開発者ブログの担当でもあります。

YenomではAndroid/iOS共にコードカバレッジをBitriseにて計測し、Codecovを用いてGitHubのPRでコードカバレッジレポートを閲覧できるようにしています。
今回はAndroidアプリのプロジェクトでのコードカバレッジ計測の仕組み作りについてお話します。

JaCoCoの導入

JaCoCoとはJavaのコードカバレッジ計測ライブラリです。
今回はjacoco-android-gradle-pluginを導入します。
これは、JaCoCoの導入、及びBuild Variants等のAndroidアプリのプロジェクト用の設定を行ってくれるものです。実はこの設定が結構煩雑でボイラープレートな為、プラグインにおまかせします。

さて、早速プラグインを導入してみましょう!
build.gradleにクラスパスを追加し、プラグインを有効にします。
この際、Groovyのバージョンがコンフリクトしてしまう可能性がある為、以下のようにexcludeしてあげます。

buildscript {
  dependencies {
    classpath('com.dicedmelon.gradle:jacoco-android:0.1.2') {
      exclude group: 'org.codehaus.groovy', module: 'groovy-all'
    }
  }
}

apply plugin: 'jacoco-android'

Robolectric対応

Robolectricを使ったテストに対応するために下記設定をbuild.gradleに追加します。

android {
    testOptions {
        unitTests.all {
            jacoco {
                includeNoLocationClasses = true
            }
        }
    }
}

Instrumented Tests対応

下記設定をbuild.graldeに追加することでInstrumented Testsに対応できます。

android {
  buildTypes {
    debug {
      testCoverageEnabled true
    }
  }
}

Kotlin対応

先述のプラグインは現在Kotlinに対応してないため*1、手動で対応します。
下記設定をbuild.gradleに追加してください。

project.afterEvaluate {
    android.applicationVariants.all { variant ->
        def realVariantName = variant.name
        def variantName = variant.name.capitalize()

        def task = project.tasks["jacocoTest${variantName}UnitTestReport"]
        def defaultExcludes = task.classDirectories.excludes

        def excludes = defaultExcludes

        def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${realVariantName}", excludes: excludes)

        task.classDirectories.excludes = excludes
        task.classDirectories += kotlinDebugTree
    }
}

引用元: Kotlin code coverage is not reported · Issue #37 · arturdm/jacoco-android-gradle-plugin · GitHub

除外の設定

実はもうカバレッジレポートは生成できるようになっているのですが、この状態ではカバレッジ計測の対象にすべきでないコードが含まれてしまうことがあります。
そのために除外の設定をしましょう。

Kotlinの設定に出てきたexcludes変数を置き換えていきます。
例えばDaggerの自動生成コードを除外する設定は以下のようになります。

def excludes = defaultExcludes + ['**/Dagger*Component.class', '**/Dagger*Component$*.*', '**/*Subcomponent$Builder.class']

**/は任意のディレクトリ以下、*は任意の文字、$はネステッドクラスを指定する時に用います。
実際にカバレッジレポートを生成してみて除外するクラスを見てみるのが良いと思います。

実行

./gradle jacocoTestPaidDebugUnitTestReport を実行するとカバレッジレポートが生成されます。
ただし、Paidの部分がProductFlavorの名前、Debugの部分がBuildTypeの名前となっているため、適宜置き換えてください。

タスクが成功すると$buildDir/reports/jacoco以下にテストレポートがXMLとHTML形式で生成されています。
手元でHTMLを開くことでカバレッジを確認することもできますし、お使いのCIでCodecovの設定をすると、GitHub上でカバレッジレポートを確認することができるようになります!!

f:id:Yuiki0627:20180415151507p:plain

まとめ

カバレッジに関する議論はたくさんありますが、数字として利用することができるのは利点があるので是非導入してみてください!

*1:PRは出ていますがマージされていません