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

в 21:31, , рубрики: android, coverage, gradle, jacoco, Разработка под android, метки:

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js