Я участвую в развитии open source проекта Apache Ignite, работая над проектом мне стало интересно оценить тестовое покрытие и вот что из этого получилось.
Покрытие тестами (tests coverage) — наиболее популярная метрика используемая при оценке качества тестирования продукта.
Это одна из немногих метрик, которая позволяет выявить зоны требующие внимания из-за риска пропуска ошибки, а также выполнить приоритизацию работ по модулям или компонентам проекта.
Наиболее простой способ получить полный отчет по оценке тестового покрытия Java проекта — это использовать coverage runner, встроенный в IntelliJ IDEA. Он позволяет в пару кликов настроить сбор метрик и запустить тесты с последующей генерацией отчета.
Тестирование в проекте Apache Ignite
В проекте Apache Ignite для тестирования используется собственный тестовый фреймворк, реализованный на базе JUnit 3. На момент написания статьи core модуль проекта содержит ~82 тысячи тестов, большинство из которых являются компонентными и требуют поднятия кластера из нескольких узлов, в том числе в разных JVM, с сопутствующей подготовкой окружения.
Стоит отметить, что обеспечение работоспособности столь огромной регрессионной базы — непростая задача. Сообщество постоянно следит за состоянием продукта и исправляет найденные ошибки в рамках инициативы "Make Teamcity Green Again".
Обозначенные особенности проекта не позволяют прогнать все тесты разом в одной JVM по следующим причинам:
- возможная ошибка OutOfMemoryError;
- возможный отказ (crash) JVM;
- возможные взаимные блокировки (deadlocks);
- невозможность старта теста из-за не остановленного узла в предыдущем тесте;
- прогон займет трое суток на одном компьютере.
Всё это делает невозможным использование IntelliJ IDEA для получения отчета по всем тестам проекта и требует применения специального подхода к решению задачи.
Подготовка и проведение оценки тестового покрытия
Основываясь на проделанной работе, был выбран наиболее надежный подход для выполнения задачи, содержащий следующие шаги:
- Определение набора тестовых классов;
- Выполнение для каждого тестового класса:
2.1. запуска и прогона набора тестов класса в отдельной JVM со сторожевым таймером, который завершит поток, в случае зависания или проблем с тестами;
2.2. операций по получению и сохранению метрик тестового покрытия;
2.3. очистки окружения по завершении тестов; - Слияние всех метрик полученных в пункте 2;
- Генерация полного отчета.
Существует множество инструментов предназначенных для оценки тестового покрытия, наиболее популярные из них:
Не буду останавливаться на их различиях, наглядная таблица сравнения возможностей инструментов для оценки тестового покрытия представлена здесь.
Для решения задачи была выбрана библиотека JaCoCo, для того, чтобы иметь возможность встроить решение на TeamCity, на которой базируется существующая инфраструктура тестирования проекта Apache Ignite. TeamCity умеет "из коробки" работать с JaCoCo.
Для автоматизации описанного алгоритма использовались bash-скрипт и Maven. Конфигурация Jacoco Maven плагина реализована отдельным Maven профилем в pom.xml.
Профиль конфигурации JaCoCo плагина приведен ниже и подразумевает разделение на 2 отдельных запуска:
- Прогон тестов с подключенным агентом JaCoCo (prepare-agent) для сбора метрик тестового покрытия. Свойство 'runDirectory' будет передаваться скриптом при запуске, что позволит сохранять результаты прогонов изолировано;
- Слияние результатов прогона (merge) и генерация отчета (report).
<profile>
<id>coverage</id>
<properties>
<argLine>
-ea -server -Xms1g -Xmx6g -XX:+HeapDumpOnOutOfMemoryError -XX:+AggressiveOpts -DIGNITE_UPDATE_NOTIFIER=false -DIGNITE_NO_DISCO_ORDER=true -DIGNITE_PERFORMANCE_SUGGESTIONS_DISABLED=true -DIGNITE_QUIET=false -Djava.net.preferIPv4Stack=true </argLine>
<coverage.dataFile>${runDirectory}/coverage-reports/jacoco-ut.exec</coverage.dataFile>
<coverage.outputDir>${runDirectory}/jacoco-ut</coverage.outputDir>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
</configuration>
<executions>
<execution>
<id>default-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${coverage.dataFile}</destFile>
</configuration>
</execution>
<execution>
<id>post-merge</id>
<phase>validate</phase>
<goals>
<goal>merge</goal>
</goals>
<configuration>
<fileSets>
<fileSet>
<directory>${basedir}</directory>
<includes>
<include>results/*/coverage-reports/jacoco-ut.exec</include>
</includes>
</fileSet>
</fileSets>
<destFile>merged.exe</destFile>
</configuration>
</execution>
<execution>
<id>generate-report</id>
<phase>validate</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${basedir}/merged.exe</dataFile>
<outputDirectory>${basedir}/coverage-report</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Ниже приведен скрипт реализующий описанные ранее шаги.
#!/bin/bash
# Проект должен быть скомпилирован в соответствии с DEVNOTES.txt
#
# Скрипт необходимо запускать в: ignite/modules/core
#
# Команда запуска скрипта: 'nohup ./coverage.sh >/dev/null 2>&1 &'
SCRIPT_DIR=$(cd $(dirname "$0"); pwd)
echo "***** Старт."
echo "***** Поиск тестовых классов..."
tests=()
while IFS= read -r -d $'\0'; do
tests+=("$REPLY")
done < <(find $SCRIPT_DIR/src/test/java/org/apache/ignite -type f -name "*Test*" ! -name "*\$*" ! -name "*Abstract*" ! -name "*TestSuite*" -print0)
testsCount=${#tests[@]}
echo "***** Количество тестовых классов="$testsCount
idx=0
for path in ${tests[@]}
do
idx=$((idx+1))
echo "***** Запуск "$idx" из "$testsCount
echo "***** Расположение класса: "$path
filename=$(basename -- "$path")
filename="${filename%.*}"
echo "***** Название класса: "$filename
runDir=$SCRIPT_DIR"/results/"$filename
mkdir -p $runDir
if [ "$(ls -A $runDir)" ]; then
continue
fi
echo "***** Запуск тестов..."
timeout 30m mvn -P surefire-fork-count-1,coverage test -Dmaven.main.skip=true -Dmaven.test.failure.ignore=true -Dtest=$filename -DfailIFNoTests=false -DrunDirectory=$runDir
echo "***** Очистка окружения..."
pkill java
done
# Объединение результатов и генерация отчета
mvn -X -P surefire-fork-count-1,coverage validate
echo "***** Финиш."
Прогон всех тестов с оценкой покрытия занял ~50 часов на выделенном сервере: 4 vCPU, 8RAM, 50 SSD, Ubuntu x64 16.04.
Описанный подход легко может быть распараллелен на несколько стендов, при наличии ресурсов, что существенно сократит время прогона и получения оценки тестового покрытия. После встраивания данного решения на TeamCity время оценки тестового покрытия должно занимать около 2-х часов.
Результаты
По результатам отчета, покрытие инструкций проекта составляет ~61%.
Покрытие инструкций основных компонентов:
- Cache – 66%
- Discovery — 57%
- Compute – 60 %
- Stream – 51 %
- Binary – 68 %
- Transactions – 71%
После анализа результатов стало очевидно, что покрыт весь горячий код, а также код по исправлению типовых проблем. Имея такой инструментарий можно будет расширять покрытие на редкие и нетипичные ситуации, делая продукт еще надежнее.
P.S. Полный отчет для ревизии.