Edited at

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.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' //ここを追加
}
:

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が必須(だけど設定でつまづきがち)。

  • この記事の設定を完了するとコードカバレッジ取得だけでなく開発が全体的に快適になる(はず..!