Автор: Mike Gouline
https://blog.gouline.net/2015/06/23/code-coverage-on-android-with-jacoco/
Перевод: Семён Солдатенко
С тех пор как эта возможность появилась в Android Gradle плагине версии 0.10.0 было написано много статей об измерении покрытия кода тестами (test coverage) — и я не испытываю никаких иллюзий по этому поводу. Однако, что меня раздражает, так это необходимость заглядывать в несколько таких статей и даже в документацию Gradle прежде чем вы получите полностью работающее решение. Так что вот, еще одна статья которая попытается это исправить и сберечь ваше время.
Постановка задачи
Имеется Android проект с модульными тестами (unit tests), и мы хотим создать отчет о покрытии кода для выполненных тестов. Решение должно поддерживать различные режимы сборки (build types) и вариации продукта (product flavours).
Решение
Решение состоит из несколько частей, поэтому давайте рассмотрим его по шагам.
Включите сбор данных о покрытии кода
Вам нужно включить поддержку сбора данных о покрытии кода тестами для режима сборки (build type) в котором вы будете выполнять тесты. Ваш build.gradle
должен содержать следующее:
android {
...
buildTypes {
debug {
testCoverageEnabled = true
}
...
}
...
}
Настройте JaCoCo
Хотя всё из этого раздела можно было бы поместить в build.gradle
, такой «навесной монтаж» сделает ваш сценарий сборки не читаемым, поэтому я рекомендую поместить всё это в отдельный сценарий сборки, а затем импортировать.
Мы начнем настройку JaCoCo с создания файла jacoco.gradle
в корневом каталоге проекта. Можете создать его в любом месте где пожелаете, но держать его в корневом каталоге проекта позволит легко на него ссылаться из всех подпроектов.
Самая простая часть — импорт JaCoCo:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.5.201505241946"
}
Обратите внимание, что вам не нужно объявлять какие-либо зависимости чтобы использовать плагин «jacoco» — всё что нужно подключит плагин Android.
Чтобы проверить какая версия последняя, поищите org.jacoco:org.jacoco.core в jCenter, но обновляйте осторожно — самая последняя версия может оказаться пока еще несовместимой, что может привести к каким-нибудь странностям, например к пустому отчету.
Следующий шаг это создание задач Gradle (Gradle tasks) для всех вариаций продукта и режимов сборки (на самом деле вы будете тестировать только отладочную сборку (debug), однако очень удобно иметь такую возможность для любой специальной конфигурации отладочной сборки).
def buildTypes = android.buildTypes.collect { type -> type.name }
def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
Обратите внимание что collect
в Groovy получает на вход список, вызывает функцию с каждым элементом списка, а результаты возвращает в новом списке. В данном случае на вход поступают списки объектов «режим сборки» и «вариация продукта» которые преобразуются в списки их названий.
В угоду проектам в которых не заданы вариации продукта мы добавим пустое имя:
if (!productFlavors) productFlavors.add('')
Теперь мы можем вот так пролистать их, что по существу является вложенным циклом в Groovy:
productFlavors.each { productFlavorName ->
buildTypes.each { buildTypeName ->
...
}
}
Самая важная часть — то, что мы поместим внутрь цикла, поэтому давайте рассмотрим это более подробно.
Сначала мы подготовим имена задач с правильной расстановкой заглавных букв:
sourceName
– название источника сборки (build source name), н-р:blueDebug
sourcePath
– путь к исходным кодам сборки (build source path), н-р:blue/debug
testTaskName
– задача для выполнения тестов от которой будет зависеть задача измерения покрытия кода, н-р:testBlueDebug
Вот как мы их определяем:
def sourceName, sourcePath
if (!productFlavorName) {
sourceName = sourcePath = "${buildTypeName}"
} else {
sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
sourcePath = "${productFlavorName}/${buildTypeName}"
}
def testTaskName = "test${sourceName.capitalize()}UnitTest"
Теперь задача, как она выглядит на самом деле:
task "${testTaskName}Coverage" (type:JacocoReport, dependsOn: "$testTaskName") {
group = "Reporting"
description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build."
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/BuildConfig.*',
'**/Manifest*.*']
)
def coverageSourceDirs = [
"src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"
]
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
reports {
xml.enabled = true
html.enabled = true
}
}
Вы могли встречать похожий код в других статьях про JaCoCo, поэтому надеюсь, что большая его часть понятна без объяснений.
Части заслуживающие дополнительного внимания:
classDirectories
– в"excludes"
вы можете перечислить шаблоны для исключения из отчета; это может быть сгенерированный код (классR
, код внедряющий зависимости и т.д.) или что-угодно, что вы захотите игнорироватьreports
– разрешает HTML и/или XML отчеты, в зависимости от того, нужны ли они для публикации или для анализа, соответственно.
Вот и всё о jacoco.gradle
, поэтому вот полное содержимое файла:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.5.201505241946"
}
project.afterEvaluate {
// Grab all build types and product flavors
def buildTypes = android.buildTypes.collect { type -> type.name }
def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
// When no product flavors defined, use empty
if (!productFlavors) productFlavors.add('')
productFlavors.each { productFlavorName ->
buildTypes.each { buildTypeName ->
def sourceName, sourcePath
if (!productFlavorName) {
sourceName = sourcePath = "${buildTypeName}"
} else {
sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
sourcePath = "${productFlavorName}/${buildTypeName}"
}
def testTaskName = "test${sourceName.capitalize()}UnitTest"
// Create coverage task of form 'testFlavorTypeCoverage' depending on 'testFlavorTypeUnitTest'
task "${testTaskName}Coverage" (type:JacocoReport, dependsOn: "$testTaskName") {
group = "Reporting"
description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build."
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/BuildConfig.*',
'**/Manifest*.*']
)
def coverageSourceDirs = [
"src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"
]
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
reports {
xml.enabled = true
html.enabled = true
}
}
}
}
}
В заключение вам нужно импортировать этот сценарий сборки в ваш сценарий в app
как-то так:
apply from: '../jacoco.gradle'
(Обратите внимание: Здесь подразумевается, что jacoco.gradle
расположен в корневой директории вашего проекта, как было описано выше)
Вот и всё! Вы можете убедиться, что задачи создаются, выполнив gradle tasks
и поискав в секции "Reporting"
что-то похожее на следующее:
Reporting tasks
---------------
testBlueDebugUnitTestCoverage - Generate Jacoco coverage reports on the BlueDebug build.
testBlueReleaseUnitTestCoverage - Generate Jacoco coverage reports on the BlueRelease build.
testRedDebugUnitTestCoverage - Generate Jacoco coverage reports on the RedDebug build.
testRedReleaseUnitTestCoverage - Generate Jacoco coverage reports on the RedRelease build.
Чтобы создать отчет, выполните gradle testBlueDebugUnitTestCoverage
и вы найдете его в "build/reports/jacoco/testBlueDebugUnitTestCoverage/"
.
Обновления
- 2015-08-23: Fixed jacoco.gradle script for Gradle plugin 1.3.0, where test tasks are suffixed with «UnitTest».
- 2015-10-01: Fixed task name suffixes in text.
- 2015-10-28: Fixed build path changes for latest Android plugin.
Исходный код
JaCoCo example (GitHub)
Автор: SamSol