
Привет, Хабр!
Меня зовут Артём Добровинский, я работаю 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. Команды прогоняются через &&, т.к., ход исполнения должен прерваться, если предыдущий шаг не закончился успехом.
Что происходит по команде выше:
- Сначала прогоняем тесты (заодно генерируем все необходимые файлы в папке build).
- Генерируем отчет Jacoco.
- Запускаем Sonarqube.
Далее надо зайти на сайт, провалиться в проект и посмотреть на масштаб бедствия. На странице проекта показывается результат последней проверки.
С Sonarqube представление о том, в каком состоянии находится проект становится намного полнее. Проще корректировать беклог техдолга, есть, чем занять начинающих разработчиков (в каждой придирке Sonarqube приводит аргументацию того, почему так писать не принято — чтение этих объяснений может быть очень полезно), да и просто — knowledge is power.
That's all, folks!
Вопрос к читателям — чем вы пользуетесь для анализа кода и измерения test coverage? Видите ли вообще в этом смысл?
