Измерение качества кода Android-приложения с помощью Sonarqube и Jacoco в 2019 году


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


    Меня зовут Артём Добровинский, я работаю 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? Видите ли вообще в этом смысл?

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

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

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое