Unit Test 探求記は、unit test に関してまったりと実験しつつその過程を綴ってみるというものです。
今回のお題
- JaCoCo plugin を導入する。
- Unit test と instrumented test の各々個別のレポートを生成する。
- Unit test と instrumented test を合成したレポートを生成する。
JaCoCo とは
- JaCoCo はコードカバレッジ用のライブラリでです。
- The JaCoCo Plugin でサポートされています。
- Android Gradle plugin でもサポートされています。
前提
今回は以下のような gradle project 構成を仮定して進めます。
$ ./gradlew -q projects
Root project 'of-commons'
+--- Project ':android'
\--- Project ':kotlin'
-
:android
は android-library 用の plugin を使用している。 -
:kotlin
は kotlin 用の plugin を使用している。
今回のゴール
以下のタスクを作成することを今回のゴールとします。
モジュール | レポートを生成するタスク | レポートの対象となるテストタスク |
---|---|---|
:kotlin | jacocoTestReport | test |
:android | jacocoTest<*Variant*>UnitTestReport | test<*Variant*>UnitTest |
jacocoTestReport | test | |
jacocoConnectedAndroidTestReport | connectedAndroidTest | |
jacocoMergeTestReport ※ | testDebugUnitTest, connectedAndroidTest | |
※ jacocoMergeTestReport は、testDebugUnitTest と connectedAndroidTest の結果を合成した単一のレポートを生成する。 |
Kotlin moduleへのJaCoCo導入
◆ コード
JaCoCo 関連の差分のみを以下に示します。
plugins {
jacoco
}
// for JaCoCo
jacoco {
toolVersion = JacocoUtils.toolVersion
}
// If you want to generate report always after tests run, please uncomment below.
// tasks.test {
// // report is always generated after tests run
// finalizedBy(tasks.jacocoTestReport)
// }
// If you want to run tests always before generating report, please uncomment below.
tasks.jacocoTestReport {
// tests are required to run before generating the report
dependsOn(tasks.test)
}
object JacocoUtils {
const val toolVersion: String = "0.8.5"
}
◆ 実行
☆ jacocoTestReport
$ ./gradlew :kotlin:jacocoTestReport
レポートは kotlin/build/reports/jacoco/test/html/ 以下に出力されます。
Android-library moduleへのJaCoCo導入
◆ コード
JaCoCo 関連の差分のみを以下に示します。
plugins {
jacoco
}
android {
buildTypes {
getByName("debug") {
isTestCoverageEnabled = true
}
}
}
jacoco {
toolVersion = JacocoUtils.toolVersion
}
afterEvaluate {
// for 'jacocoTestXxxUnitTestReport' task (e.g. jacocoTestDebugUnitTestReport)
android.libraryVariants.forEach { variant ->
val variantName = variant.name
val capitalizedVariantName = variantName.capitalize()
createJacocoReportTask(
"jacocoTest${capitalizedVariantName}UnitTestReport",
variantName,
"Generates code coverage report for the test${capitalizedVariantName}UnitTest task.",
"test${capitalizedVariantName}UnitTest"
)
}
// for 'jacocoTestReport' task
task<JacocoReport>("jacocoTestReport") {
android.libraryVariants.forEach { variant ->
dependsOn("jacocoTest${variant.name.capitalize()}UnitTestReport")
}
}
// for 'jacocoConnectedAndroidTestReport' task
createJacocoReportTask(
"jacocoConnectedAndroidTestReport",
"debug",
"Generates code coverage report for the connectedAndroidTest task.",
"connectedAndroidTest"
)
// for 'jacocoMergeTestReport' task
val testDebugUnitTest = "testDebugUnitTest"
val connectedAndroidTest = "connectedAndroidTest"
val description = "Generates code coverage report for the $testDebugUnitTest and $connectedAndroidTest tasks."
createJacocoReportTask(
"jacocoMergeTestReport",
"debug",
description,
testDebugUnitTest,
connectedAndroidTest
)
}
以下は、タスク生成に関する共通ロジックを buildSrc 側に抜き出したものです。
build.gradle.kts 内部に記述しても問題はありません。
import org.gradle.api.Project
// import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.task
import org.gradle.testing.jacoco.tasks.JacocoReport
object JacocoUtils {
const val toolVersion: String = "0.8.5"
fun Project.createJacocoReportTask(reportTaskName: String, variantName: String, description: String, vararg testTaskNames: String) {
task<JacocoReport>(reportTaskName) {
testTaskNames.forEach { testTaskName ->
dependsOn(testTaskName)
// テストタスク完了時に必ずレポートタスクを実行させたい場合はアンコメントしてください。
// report is always generated after tests run
// tasks[testTaskName].finalizedBy(tasks[reportTaskName])
}
group = "verification"
this.description = description
// The following are the default settings for 'reports'
// reports {
// csv.isEnabled = false
// html.isEnabled = true
// xml.isEnabled = false
// }
// Exclude the class files corresponding to the auto-generated source files.
// TODO: 実際に目視確認したもののみを除外していく。(例えば R.class はまだ実際に目視確認してないので除外していない)
val classDirectoriesTreeExcludes = setOf(
// e.g. class : androidx/databinding/library/baseAdapters/BR.class
// element : BR
"androidx/**/*.class",
// e.g. class : <AndroidManifestPackage>/DataBinderMapperImpl.class
// element : DataBinderMapperImpl
"**/DataBinderMapperImpl.class",
// e.g. class : <AndroidManifestPackage>/DataBinderMapperImpl$InnerBrLookup.class
// element : DataBinderMapperImpl.InnerBrLookup
"**/DataBinderMapperImpl\$*.class",
// e.g. class : <AndroidManifestPackage>/BuildConfig.class
// element : BuildConfig
"**/BuildConfig.class",
// e.g. class : <AndroidManifestPackage>/BR.class
// element : BR
"**/BR.class",
// e.g. class : <AndroidManifestPackage>/DataBindingInfo.class
// element : DataBindingInfo
"**/DataBindingInfo.class"
// "**/R.class",
// "**/R$*.class",
// "**/Manifest*.*",
// "android/**/*.*",
// "**/Lambda$*.class",
// "**/*\$Lambda$*.*",
// "**/Lambda.class",
// "**/*Lambda.class",
// "**/*Lambda*.class",
// "**/*Lambda*.*",
// "**/*Builder.*"
)
// classDirectories --------------------------------------------------------------------
val javaClassDirectoriesTree = fileTree(
mapOf(
"dir" to "${buildDir}/intermediates/javac/${variantName}/classes/",
"excludes" to classDirectoriesTreeExcludes
)
)
val kotlinClassDirectoriesTree = fileTree(
mapOf(
"dir" to "${buildDir}/tmp/kotlin-classes/${variantName}",
"excludes" to classDirectoriesTreeExcludes
)
)
classDirectories.setFrom(files(javaClassDirectoriesTree, kotlinClassDirectoriesTree))
// sourceDirectories -------------------------------------------------------------------
val mainSourceDirectoryRelativePath = "src/main/java"
val variantSourceDirectoryRelativePath = "src/${variantName}/java"
sourceDirectories.setFrom(
mainSourceDirectoryRelativePath,
variantSourceDirectoryRelativePath
)
// executionData -----------------------------------------------------------------------
executionData.setFrom(
fileTree(
mapOf(
"dir" to project.projectDir,
"includes" to listOf(
// unit test execution data
"**/*.exec",
// instrumented test execution data
"**/*.ec"
)
)
)
)
}
}
}
◆ 実行
☆ jacocoTestDebugUnitTestReport
testDebugUnitTest タスクによるテストカバレッジをレポートします。
$ ./gradlew :android:jacocoTestDebugUnitTestReport
レポートは android/build/reports/jacoco/jacocoTestDebugUnitTestReport/html/ 以下に出力されます。
☆ jacocoTestReleaseUnitTestReport
testReleaseUnitTest タスクによるテストカバレッジをレポートします。
$ ./gradlew :android:jacocoTestReleaseUnitTestReport
レポートは android/build/reports/jacoco/jacocoTestReleaseUnitTestReport/html/ 以下に出力されます。
☆ jacocoTestReport
test タスクによるテストカバレッジをレポートします。
$ ./gradlew :android:jacocoTestReport
test<*Variant*>UnitTest に当てはまるすべてのレポートが生成されます。
※ 上記2例と同様なのでレポート表示は割愛。
☆ jacocoConnectedAndroidTestReport
connectedAndroidTest タスクによるテストカバレッジをレポートします。
$ ./gradlew :android:jacocoConnectedAndroidTestReport
レポートは android/build/reports/jacoco/jacocoConnectedAndroidTestReport/html/ 以下に出力されます。
☆ jacocoMergeTestReport
testDebugUnitTest タスクと connectedAndroidTest タスクによるテストカバレッジを統合してレポートします。
$ ./gradlew :android:jacocoMergeTestReport
レポートは android/build/reports/jacoco/jacocoMergeTestReport/html/ 以下に出力されます。
文字化け対策
レポートが文字化けする場合は、gradle.properties ファイルに以下の設定を追加してください。1
org.gradle.jvmargs=-Dfile.encoding=UTF-8
まとめ
- JaCoCo plugin を導入をしてみました。
- 今回 android についてはライブラリプロジェクトに対応しましたが、
android.libraryVariants
の個所をandroid.applicationVariants
にするだけでアプリケーションプロジェクトにも対応できると思います。 - classDirectoriesTreeExcludes については今後の課題ということで。
- flavor については今後の課題ということで。
- GitHub のソースはこちら。
-
裏は取ってないけど Windows 環境で発生するっぽいです。 ↩