はじめに
この記事では以下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.gradle
に testCoverageEnabled
を追記するだけです。
android {
:
buildTypes {
debug {
testCoverageEnabled true
}
}
}
syncすると gradle task
に以下のタスクが追加されます。
verification: createDebugCoverageReport
GUI上でクリックするか以下コマンド実行するとレポートが作成されます。
(AndroidTestのため、エミュレータもしくは実機を接続している必要があります)
./gradlew createDebugCoverageReport
作成されるファイルは以下の通りです。
.ecはAndroidTestのバイナリファイルとなります。Jacoco内部ではecファイルをhtmlファイルに出力する処理を行います。
build/output/code_coverage/debugAndroidTest/connected/{デバイス名}-coverage.ec
build/output/reports/coverage/debug/index.html
ここまでで一見良さそうなのですが以下の問題点があります。
問題点1: UnitTestの結果が出力されない
上記のプロジェクトでCalculatorというクラスを作成し、test
フォルダ内にテストコードを追加してみます。
同じく、 createDebugCoverageReport
を実行すると..
UnitTestの内容はレポートに反映されていないことがわかります。
これは、AndroidTestに使用されるAndroidJUnitRunnerにはレポート作成機能を内包しているが通常のJUnitRunnerはレポート作成を実行しないためです。
参考: AndroidJUnitRunnerの動作 (google I/O '17スライドより)
対応方法は後述します。
問題点2: Kotlinのコードに対応していない
JacocoはJava向けのCodeCoverageツールより、一部Kotlinの内容と合わないことがあります。
対応方法は後述します。
AndroidTestとUnitTestをマージしKotlin対応するJacoco設定
上記の問題点でUnitTestにもKotlinにも対応させる2020年版とも言えるjacoco設定は以下となります。
:
apply from: './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対応したコードカバレッジを出力します。
↓AndroidtestとUnitTestがマージされている
↓Kotlin対応されている
これでコードカバレッジをレポート出力して共有することが可能になりました。
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
となっていることを確認します。
test
フォルダを右クリックするとRun with Coverage
と表示されます。
Android Studio組み込みなので、手軽に実行できすぐ結果がわかるのが特徴です。
以下緑の行がテストされていて赤い箇所が未テストの行です。
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
を追加します。
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共通のテストコードとなります。
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
が出現します。
設定を行うのはjava8の検証をOFFにする設定です。
i) GUI用にRun ConfigurationsのVM Optionで -noverify
を追加します。
ii) CUI用にapp.gradleに jvmArgs '-noverify'
を追加します。
:
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が必須(だけど設定でつまづきがち)。
- この記事の設定を完了するとコードカバレッジ取得だけでなく開発が全体的に快適になる(はず..!