LoginSignup
42
33

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-16

はじめに

この記事では以下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

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

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

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

Jacocoセットアップと実行

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

Jack now supports Jacoco test coverage when setting testCoverageEnabled to true.

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

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

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

verification: createDebugCoverageReport

スクリーンショット 2019-06-17 7.12.44.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

スクリーンショット 2019-06-17 5.35.49.png

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

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

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

スクリーンショット 2019-06-17 7.24.02.png

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

スクリーンショット 2019-06-17 6.30.19.png

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

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

参考: AndroidJUnitRunnerの動作 (google I/O '17スライドより)
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f31393639322f32383532343265362d303962352d353164352d636563302d3137623963666662646265312e706e67 (1).png

対応方法は後述します。

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

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

↓コードは全部通っているが..
スクリーンショット 2019-06-17 7.29.20.png

↓通っていないメソッドがある??
スクリーンショット 2019-06-17 7.29.10.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対応したコードカバレッジを出力します。

スクリーンショット 2019-06-17 7.38.03.png

↓AndroidtestとUnitTestがマージされている
スクリーンショット 2019-06-17 7.39.59.png
↓Kotlin対応されている
スクリーンショット 2019-06-17 7.40.06.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 となっていることを確認します。
スクリーンショット 2019-06-17 7.47.51.png

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

スクリーンショット 2019-06-17 6.53.41.png

Android Studio組み込みなので、手軽に実行できすぐ結果がわかるのが特徴です。
以下緑の行がテストされていて赤い箇所が未テストの行です。
スクリーンショット 2019-06-17 7.47.14.png

Robolectric 4.0と組み合わせる

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

が!

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

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

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 が出現します。

スクリーンショット 2019-05-19 0.33.05.png

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

i) GUI用にRun ConfigurationsのVM Optionで -noverify を追加します。
スクリーンショット 2019-05-19 0.42.33.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' //ここを追加
        }
    :

 -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が必須(だけど設定でつまづきがち)。
  • この記事の設定を完了するとコードカバレッジ取得だけでなく開発が全体的に快適になる(はず..!
42
33
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
42
33