Измерение покрытия кода тестами в Android с помощью JaCoCo

от автора

Автор: 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *