[Из песочницы] Оценка тестового покрытия Java проекта на примере Apache Ignite

Я участвую в развитии open source проекта Apache Ignite, работая над проектом мне стало интересно оценить тестовое покрытие и вот что из этого получилось.

ninx2fu1w2kkcjfb0qlbydgdzru.jpeg

Покрытие тестами (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 для получения отчета по всем тестам проекта и требует применения специального подхода к решению задачи.


Подготовка и проведение оценки тестового покрытия

Основываясь на проделанной работе, был выбран наиболее надежный подход для выполнения задачи, содержащий следующие шаги:


  1. Определение набора тестовых классов;
  2. Выполнение для каждого тестового класса:
    2.1. запуска и прогона набора тестов класса в отдельной JVM со сторожевым таймером, который завершит поток, в случае зависания или проблем с тестами;
    2.2. операций по получению и сохранению метрик тестового покрытия;
    2.3. очистки окружения по завершении тестов;
  3. Слияние всех метрик полученных в пункте 2;
  4. Генерация полного отчета.

Существует множество инструментов предназначенных для оценки тестового покрытия, наиболее популярные из них:

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

Для решения задачи была выбрана библиотека JaCoCo, для того, чтобы иметь возможность встроить решение на TeamCity, на которой базируется существующая инфраструктура тестирования проекта Apache Ignite. TeamCity умеет «из коробки» работать с JaCoCo.

Для автоматизации описанного алгоритма использовались bash-скрипт и Maven. Конфигурация Jacoco Maven плагина реализована отдельным Maven профилем в pom.xml.

Профиль конфигурации JaCoCo плагина приведен ниже и подразумевает разделение на 2 отдельных запуска:


  1. Прогон тестов с подключенным агентом JaCoCo (prepare-agent) для сбора метрик тестового покрытия. Свойство 'runDirectory' будет передаваться скриптом при запуске, что позволит сохранять результаты прогонов изолировано;
  2. Слияние результатов прогона (merge) и генерация отчета (report).


Maven конфигурация JaCoCo

 coverage

 
    
          -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 \
    
    ${runDirectory}/coverage-reports/jacoco-ut.exec
    ${runDirectory}/jacoco-ut
 

 
    
       
          org.apache.maven.plugins
          maven-surefire-plugin
          
          
          
             
                default-test
                test
                
                   test
                
             
          
       
       
          org.jacoco
          jacoco-maven-plugin
          0.8.1
          
             
                default-prepare-agent
                
                   prepare-agent
                
                
                   ${coverage.dataFile}
                
             
             
                post-merge
                validate
                
                   merge
                
                
                   
                      
                         ${basedir}
                         
                            results/*/coverage-reports/jacoco-ut.exec
                         
                      
                   
                   merged.exe
                
             
             
                generate-report
                validate
                
                   report
                
                
                   ${basedir}/merged.exe
                   ${basedir}/coverage-report
                
             
          
       
    
 

Ниже приведен скрипт реализующий описанные ранее шаги.


Управляющий bash-cкрипт
#!/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. Полный отчет для ревизии.

© Habrahabr.ru