Автор: 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)
ссылка на оригинал статьи https://habrahabr.ru/post/280374/
Добавить комментарий