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