Android開発のテストカバー率取得にはこのツールを使い分けると良いという話

# はじめに

この記事では以下Androidのコードカバレッジツール(テストカバー率を測定するツール)の紹介をします。

- Jacoco
- IntelliJ Code Coverage

このタイミングで書きたくなったのは以下の理由のためです。

- Jacocoは一見セットアップが簡単だけどKotlin対応などで追加の対応が必要になったため
- Robolectric4.0の登場でIntelliJ Code Coverageが格段に使いやすくなったため
- もうJacoco+IntelliJ Code Coverage+Robolectricなしでは開発しづらいと思うようになってしまったため

この記事の開発環境は以下となります。

- MacOS 10.14
- Android Studio 3.4

# Jacoco

https://www.jacoco.org/jacoco/

Jacocoはその名の通りJavaのためのCode Coverageツールです。
最初のバージョン0.1.0は2009年に公開され、2019年6月現在最新バージョンは0.8.4です。

Jacocoはhtmlレポートも出力でき、出力項目を自由に拡張できるのが特徴です。

- 得意: チーム内外にカバレッジ状況を共有する
- 苦手: 頻繁にカバレッジを確認する

## Jacocoセットアップと実行

Android Studio 2.2では公式にJacocoがサポートされました。

https://developer.android.com/studio/releases/gradle-plugin
> Jack now supports Jacoco test coverage when setting testCoverageEnabled to true.

このため、セットアップは容易で`app.gradle`に `testCoverageEnabled`を追記するだけです。

```app.gradle
android {
:
    buildTypes {
        debug {
            testCoverageEnabled true
        }
    }
}
```

syncすると `gradle task` に以下のタスクが追加されます。

```
verification: createDebugCoverageReport
```

<img width="800" alt="スクリーンショット 2019-06-17 7.12.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/d7b836f0-316e-7ecf-f8be-cb6e01291387.png">


GUI上でクリックするか以下コマンド実行するとレポートが作成されます。
(AndroidTestのため、エミュレータもしくは実機を接続している必要があります)

```
./gradlew createDebugCoverageReport
```

作成されるファイルは以下の通りです。
.ecはAndroidTestのバイナリファイルとなります。Jacoco内部ではecファイルをhtmlファイルに出力する処理を行います。

```
build/output/code_coverage/debugAndroidTest/connected/{デバイス名}-coverage.ec
build/output/reports/coverage/debug/index.html
```
<img width="320" alt="スクリーンショット 2019-06-17 5.35.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/4a0dc44c-7cfb-b62d-2443-7d60fefa0ef5.png">

ここまでで一見良さそうなのですが以下の問題点があります。

### 問題点1: UnitTestの結果が出力されない

上記のプロジェクトでCalculatorというクラスを作成し、`test` フォルダ内にテストコードを追加してみます。

<img width="800" alt="スクリーンショット 2019-06-17 7.24.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/c76b4038-c1cc-24a4-b93b-ee60b78bd588.png">


同じく、 `createDebugCoverageReport` を実行すると..

<img width="800" alt="スクリーンショット 2019-06-17 6.30.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/c805333c-1d05-cbaf-7169-acbc08e0157b.png">

UnitTestの内容はレポートに反映されていないことがわかります。

これは、AndroidTestに使用されるAndroidJUnitRunnerにはレポート作成機能を内包しているが通常のJUnitRunnerはレポート作成を実行しないためです。

参考: AndroidJUnitRunnerの動作 (google I/O '17スライドより)
<img width="800" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f31393639322f32383532343265362d303962352d353164352d636563302d3137623963666662646265312e706e67 (1).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/f1d405f3-251e-c639-9b36-26c182611c50.png">

対応方法は後述します。

### 問題点2: Kotlinのコードに対応していない

JacocoはJava向けのCodeCoverageツールより、一部Kotlinの内容と合わないことがあります。

↓コードは全部通っているが..
<img width="640" alt="スクリーンショット 2019-06-17 7.29.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/777e0fe9-404f-3e55-be30-cb91aaa571ed.png">

↓通っていないメソッドがある??
<img width="640" alt="スクリーンショット 2019-06-17 7.29.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/d40d140f-ffa5-31e2-56b9-6b8f0910f52d.png">

対応方法は後述します。

## AndroidTestとUnitTestをマージしKotlin対応するJacoco設定

上記の問題点でUnitTestにもKotlinにも対応させる2020年版とも言えるjacoco設定は以下となります。

```app.gradle
:
apply from: './jacoco.gradle' //最終行に追加

```

```jacoco.gradle
apply plugin: 'jacoco'

jacoco {
  toolVersion = "0.8.4" //ツールバージョンを指定可能。省略可。
}

android.applicationVariants.all { variant ->
    def variantName = variant.name.capitalize() //ex. ProdDebug
    def realVariantName = variant.name //ex. prodDebug

    if (variant.buildType.name != "debug") {
        return
    }

    task("jacoco${variantName}TestReport", type: JacocoReport) {
        //AndroidTest後にUnitTestの内容をマージします。
        dependsOn "create${variantName}CoverageReport"
        dependsOn "test${variantName}UnitTest"

        group = "testing"
        description = "Generate Jacoco coverage reports for ${realVariantName}"

        reports {
            xml.enabled = false
            html.enabled = true
        }

        //無視するファイル(excludes)の設定を行います
        def fileFilter = ['**/R.class',
                          '**/R$*.class',
                          '**/BuildConfig.*',
                          '**/Manifest*.*',
                          'android/**/*.*',
                          'androidx/**/*.*',
                          '**/Lambda$*.class',
                          '**/Lambda.class',
                          '**/*Lambda.class',
                          '**/*Lambda*.class',
                          '**/*Lambda*.*',
                          '**/*Builder.*'
        ]
        def javaDebugTree = fileTree(dir: "${buildDir}/intermediates/javac/${realVariantName}/compile${variantName}JavaWithJavac/classes", excludes: fileFilter)
        def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${realVariantName}", excludes: fileFilter)

        def mainSrc = "${project.projectDir}/src/main/java"

        getSourceDirectories().setFrom(files([mainSrc]))
        //Java, Kotlin混在ファイル対応
        getClassDirectories().setFrom(files([javaDebugTree, kotlinDebugTree]))
        getExecutionData().setFrom(fileTree(dir: project.projectDir, includes: [
                '**/*.exec',    //JUnit Test Result
                '**/*.ec'])     //Espresso Test Result
        )
    }
}
```

上記で定義したタスク `jacocoDebugTestReport` を実行すると `report/jacoco` フォルダ内にUnitTest, Kotlin対応したコードカバレッジを出力します。

<img width="800" alt="スクリーンショット 2019-06-17 7.38.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/efa13233-5033-b60a-0957-6be81492d546.png">

↓AndroidtestとUnitTestがマージされている
<img width="640" alt="スクリーンショット 2019-06-17 7.39.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/6edbe04b-0b30-77a5-9f74-7fd49f366820.png">
↓Kotlin対応されている
<img width="640" alt="スクリーンショット 2019-06-17 7.40.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/6a9c5827-5cb1-5315-dfd2-a07f068fe9b1.png">

これでコードカバレッジをレポート出力して共有することが可能になりました。

# IntelliJ Code Coverage

> IntelliJ IDEA 2019.1 Help - Code Coverage
> https://www.jetbrains.com/help/idea/code-coverage.html

IntelliJ Code CoverageはAndroid Studioに組み込まれているコードカバレッジツールです。

- 得意: 頻繁にカバレッジを確認する
- 苦手: チーム内外にカバレッジ状況を共有する

## とりあえず実行してみる

テスト実行(Run Configuration)でCode Coverageタブ内のrunnerが `IntelliJ IDEA` となっていることを確認します。
<img width="800" alt="スクリーンショット 2019-06-17 7.47.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/e5b4aa99-83a8-2fe6-3de9-ff9701390447.png">

`test` フォルダを右クリックすると`Run with Coverage` と表示されます。

<img width="800" alt="スクリーンショット 2019-06-17 6.53.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/8f2a1c83-1b6f-947f-c062-4a458c205cdf.png">

Android Studio組み込みなので、手軽に実行できすぐ結果がわかるのが特徴です。
以下緑の行がテストされていて赤い箇所が未テストの行です。
<img width="800" alt="スクリーンショット 2019-06-17 7.47.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/2afb43ab-b707-38fc-28e9-a2c61232be56.png">

## Robolectric 4.0と組み合わせる

IntelliJ Code Coverageには弱点があり、`test`フォルダに対しては実行できますが、`androidTest`フォルダには実行できません。

が！

Robolectric 4.0の登場でandroidに関するテストも容易に`test`フォルダで実行可能になりました。
Robolectric 4.0とはそもそもなんだという方は前回記事を参考にしてください。
[2018年までのAndroidテスト総まとめ - 今年の変更と来年の対策](https://qiita.com/keidroid/items/c45d888759478cde1cfc)

セットアップは以下公式ページの通り行ってください。

> Robolectric - Getting Started
> http://robolectric.org/getting-started/

公式ページの補足として、gradleに`androidx.test.ext` を追加します。

```app.gradle
dependencies {
:
    testImplementation 'junit:junit:4.12'

    // AndroidX Testing Library (test)
    testImplementation 'androidx.test:core:1.2.0'
    testImplementation 'androidx.test:rules:1.2.0'
    testImplementation 'androidx.test:runner:1.2.0'
    testImplementation 'androidx.test.ext:junit:1.1.1' //ここ
    testImplementation 'androidx.test.ext:truth:1.2.0'
    // AndroidX Testing Library (androidTest)
    androidTestImplementation 'androidx.test:core:1.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1' //ここ
    androidTestImplementation 'androidx.test.ext:truth:1.2.0'

    // Robolectric
    testImplementation 'org.robolectric:robolectric:4.2'
}
```

上記設定を行うと `@RunWith(AndroidJUnit4::class)` でandroidTest/test共通のテストコードとなります。

```ExampleInstrumentedTest.kt
package red.torch.coveragesample

//import androidx.test.runner.AndroidJUnit4
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
//        val appContext = InstrumentationRegistry.getTargetContext()
        val appContext = ApplicationProvider.getApplicationContext<Context>()
        assertEquals("red.torch.coveragesample", appContext.packageName)
    }
}

```

### [optional] sharedTestでもっと便利に

Build Testable Apps for Android (Google I/O '19)
https://www.youtube.com/watch?v=VJi2vmaQe6w

ここまでの設定で以下のようにRobolectricで高速にandroidに関するテストを実行可能になりましたが、JVMで実行したい場合と実機で実行したい場合があります。


|フォルダ|実行時間|実行環境|
|:--|:--|:--|
|androidTest|遅|Device/Emulator|
|test|高速|Robolectric|

Google I/O '19ではこのような場合の解決方法が紹介されました。

build.gradleに以下のような設定を追記することにより
testでもandroidTestでも実行可能なsharedTestを追加します。

```
android {
    sourceSets {
        String sharedTestDir = 'src/sharedTest/java'
        test {
            java.srcDir sharedTestDir
        }
        androidTest {
            java.srcDir sharedTestDir
        }
    }
} 
```

これにより実行環境をRobolectricかDevice/Emulatorか柔軟に対応できるようになります。

|フォルダ|実行時間|実行環境|
|:--|:--|:--|
|androidTest|遅|Device/Emulator|
|sharedTest|-|Device/Emulator or Robolectric(JVM)|
|test|高速|JVM|

上記は必須ではありませんが対応するとより便利になります。

## IntelliJ Code Coverage + Robolectricの注意点

この組み合わせは1箇所設定しないと正常に動作しません。
未設定だと以下のように `java.lang.VerifyError` が出現します。

<img width="800" alt="スクリーンショット 2019-05-19 0.33.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/4a83060c-73ed-a99b-cf73-cd45fb4bfe84.png">

設定を行うのはjava8の検証をOFFにする設定です。

i) GUI用にRun ConfigurationsのVM Optionで `-noverify` を追加します。
<img width="800" alt="スクリーンショット 2019-05-19 0.42.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19692/1a7593ed-dec0-03b6-c4e2-bddeff3dc497.png">

ii) CUI用にapp.gradleに `jvmArgs '-noverify'` を追加します。

```app.gradle
:
    testOptions {
        //Robolectrics: include Android Resource
        unitTests.includeAndroidResources = true
        //Robolectrics: No Verify for Java 8
        unitTests.all {
            jvmArgs '-noverify' //ここを追加
        }
    :
```

https://docs.oracle.com/cd/E82638_01/jjdev/loadjava-tool.html

 ```
 -noverify
クラスがバイトコードの検証なしにロードされます。このオプションを使用するには、oracle.aurora.security.JServerPermission(Verifier)を付与されていることが必要です。このオプションを効果的にするには、-resolveと併用する必要があります。
```

上記設定を行うことでVerifyをスキップするため正常に動作します。
テストを頻繁に回してコードカバレッジ確認することが可能になりました。

参考: 
https://github.com/robolectric/robolectric/issues/3023
https://stackoverflow.com/questions/32315978/jvm-options-in-android-when-run-gradlew-test


# まとめ

- JacocoとIntelliJ Code Coverageはお互いの短所を補うので組み合わせると強い。
- JacocoはKotlin対応などで設定が少し面倒になった。
- IntelliJ Code Coverageをフル活用するにはRobolectric4.0が必須(だけど設定でつまづきがち)。
- この記事の設定を完了するとコードカバレッジ取得だけでなく開発が全体的に快適になる(はず..!