Привет, Хабр!


Меня зовут Артём Добровинский, я работаю Android-разработчиком в компании FINCH.


Однажды, за парой пинт с коллегой из компании, которая занимается размещением объявлений по продаже комиссованных МИГ'ов и комаров по имени Игорь, мы начали обсуждать статические анализаторы кода в CI (а что еще обсуждать). Прозвучала мысль о том, что их круто использовать — но только после того, как появится уверенность в логической надежности кода. Другими словами, о кодстайле можно думать только после того, как все тесты написаны.


Решил прислушаться к коллеге и задумался о том, как подсчитать масштаб бедствия для подручных приложений. Взгляд пал на Sonarqube и Jacoco. Процесс их подключения для hello-world проектов элементарен. Подключить их в Android-проект, разбитый на модули — уже сложнее. С целью помочь интересующимся и была написана эта статья.


На Хабре уже есть очень хороший перевод туториала по использованию Sonarqube — но он от 2016 года, там кое-что устарело, нет котлина да и просто я нахожу избыточным генерацию отчетов для всех buildType'ов.


Немного о библиотеках, для тех, кто с ними не знаком.


Sonarqube — это платформа с открытым исходным кодом для непрерывного анализа (continuous inspection) и измерения качества кода. Он позволяет отслеживать борьбу с техническим долгом в динамике (это классно — видеть, что технический долг побеждает, и ты не можешь ничего c этим сделать). Также Sonar отслеживает дубликаты кода, потенциальные уязвимости и чрезмерную сложность функций.


Jacoco — это бесплатная библиотека для подсчета test coverage проекта в Java. Но с котлином мы её подружим.


Как подключить Sonarqube и Jacoco


В build.gradle корневого модуля надо добавить следующий код:


apply plugin: 'android.application'
apply plugin: 'org.sonarqube'

sonarqube {
    properties {
        property "sonar.host.url", "%url домена на sonarqube%"
        property "sonar.login", "%логин%"
        property "sonar.projectName", "%имя проекта%"
        property "sonar.projectKey", "%уникальный идетификатор проекта%"
        property "sonar.reportPath", "${project.buildDir}/sonarqube/test.exec" 
        property "sonar.projectBaseDir", "$rootDir"
        property "sonar.sources", "."
        property "sonar.tests", ""
        property "sonar.coverage.exclusions", "**/src/androidTest/**, **/src/test/**"
        property "sonar.coverage.jacoco.xmlReportPaths", fileTree(include: ['*/*/jacoco*.xml'], dir: "$rootDir/app/build/reports/jacoco").collect()
    }
}

sonar.reportPath — указываем, куда Sonar должен положить отчет для последующего анализа.
sonar.projectBaseDir указываем папку, в которой изначально будет запущен анализ; в нашем случае это $rootDir — корневая папка проекта.
sonar.coverage.exclusions перечисление исключений для подсчета coverage, где ** — любая папка, a * — любое название или разрешение файла.
sonar.sources — папка с исходным кодом.
sonar.tests — пустая строка здесь для того, чтобы тесты тоже поддавались анализу Sonarqube.
sonar.coverage.exclusions — исключаем тесты из анализа test coverage.
sonar.coverage.jacoco.xmlReportPaths — с помощью collect() собираем отчеты Jacoco для подсчета test coverage.


Для активации Jacoco лучше создать файл jacoco.gradle и прописать всю необходимую логику там. Это поможет не захламлять прочие build.gradle.


Чтобы не прописывать Jacoco в build.gradle каждого подпроекта прописываем его инициализацию в замыкании subprojects. В reportsDirPath для подмодулей указываем корневую папку. Оттуда Sonar будет брать все отчеты Jacoco.


subprojects {
    apply plugin: 'jacoco'
    jacoco {
        toolVersion = '0.8.5'
        def reportsDirPath = "${project.rootDir}/app/build/reports/jacoco/${project.name}"
        reportsDir = file(reportsDirPath)
    }
}

В этот же файл прописываем функцию для настройки Jacoco.
Эта функция большая, поэтому сначала приведу её — а потом объясню, что в ней происходит.


def configureJacoco = { project ->
    def variantName = project.name
    project.tasks.create(name: "getJacocoReports", type: JacocoReport) {
        group = "Reporting"
        description = "Generate Jacoco coverage reports for the $variantName build."
        reports {
            html.enabled = true
            xml.enabled = true
        }
        def excludes = [
                '**/R.class',
                '**/R$*.class',
                '**/BuildConfig.*',
                '**/Manifest*.*',
                '**/AndroidManifest.xml',
                '**/*Test*.*',
                'android/**/*.*',
                'androidx/**/*.*', 
                '**/*Fragment.*', 
                '**/*Activity.*', 
                '**/*Api.*', 
                '**/injection/**/*.class',
                '**/ui/**/*.class', 
                %пути до build-файлов библиотек%
        ]

        def javaClasses = fileTree(dir: "${project.buildDir}/intermediates/javac", excludes: excludes)
        def kotlinClasses = fileTree(dir: "${project.buildDir}/tmp/kotlin-classes", excludes: excludes)
        classDirectories = files([javaClasses, kotlinClasses])

        sourceDirectories = files([
                "${project.projectDir}/src/main/java",
                "${project.projectDir}/src/main/kotlin",
        ])

        executionData = files(fileTree(include: ['*.exec'], dir: "${project.buildDir}/jacoco").files)
    }
}

Мы создали таск getJacocoReports, группы «Reporting». Отчеты будут предоставлены в html и xml форматах. Будут проанализированы все файлы кроме тех, что входят в массив excludes. Помимо генерируемых андройдовских файлов я решил исключить из анализа также все фрагменты и активити, интерфесы Retrofit, package с DI, кастомные вью и код библиотек.
Возможно, этот список со временем изменится.


classDirectories — указание на то, где искать код для анализа. Включаем сюда как java так и kotlin файлы.
sourceDirectories — указываем, где Jacoco искать файлы с исходным кодом.
executionData — как и в случае с Sonar, указание на то, где будет сгенерирован отчет для подсчета coverage.


Также в jacoco.gradle надо добавить его настройку для всех модулей с помощью вышеупомянутой функции:


allprojects { project ->
    configureJacoco(project)
    project.tasks.withType(Test) {
        enabled = true
        jacoco.includeNoLocationClasses = true
    }
}

И таск для сбора сгенерированных отчетов:


task getJacocoReports() {
    group = "Reporting"
    subprojects.forEach { subproject ->
        subproject.tasks.withType(JacocoReport).forEach { task ->
            dependsOn task
        }
    }
}

Запуск Sonarqube через командную строку


Запускается всё просто: ./gradlew %таск с тестами для сборки% && ./gradlew jacocoAggregateReports && ./gradlew sonarqube. Команды прогоняются через &&, т.к., ход исполнения должен прерваться, если предыдущий шаг не закончился успехом.


Что происходит по команде выше:


  1. Сначала прогоняем тесты (заодно генерируем все необходимые файлы в папке build).
  2. Генерируем отчет Jacoco.
  3. Запускаем Sonarqube.

Далее надо зайти на сайт, провалиться в проект и посмотреть на масштаб бедствия. На странице проекта показывается результат последней проверки.


С Sonarqube представление о том, в каком состоянии находится проект становится намного полнее. Проще корректировать беклог техдолга, есть, чем занять начинающих разработчиков (в каждой придирке Sonarqube приводит аргументацию того, почему так писать не принято — чтение этих объяснений может быть очень полезно), да и просто — knowledge is power.


That's all, folks!


Вопрос к читателям — чем вы пользуетесь для анализа кода и измерения test coverage? Видите ли вообще в этом смысл?

Комментарии (2)


  1. rolaman
    14.11.2019 23:33

    Крутая статья
    Сложно конечно в Сонаре всё зеленым поддерживать


  1. RodgersDevelop
    15.11.2019 21:16

    Вижу в этом смысл, когда реально тесты написаны, и ты уже не знаешь чем себя занять. Для меня реальность такова что обычно не успеваешь даже половину запланированных тестов написать. Было бы интересно узнать какой процент счастливчиков, которые написали все тесты