Введение
Покрытие кода тестами (code coverage) показывает, какая доля строк/веток кода была выполнена хотя бы раз во время прогона тестового набора. Это не метрика качества тестов само по себе (100% покрытие не означает отсутствие багов), но полезный индикатор того, какие части кода вообще никогда не проверялись. В статье настроим сбор покрытия для Qt-проекта с использованием gcov (встроенного в GCC инструмента) и lcov (для генерации читаемых HTML-отчётов).
Концепция
gcov собирает данные о покрытии, если код скомпилирован с флагами --coverage (включает генерацию инструментированного кода, который во время выполнения записывает счётчики выполнения строк в .gcda-файлы рядом с объектными .gcno-файлами). После выполнения тестов gcov или, что удобнее, lcov обрабатывает эти файлы и генерирует отчёт — текстовый или HTML с построчной разметкой покрытых/непокрытых строк.
Для CMake-проектов флаги покрытия добавляются как опции компилятора и линкера, обычно условно — только для специальной сборки с покрытием, а не для обычной Release/Debug сборки, поскольку инструментация снижает производительность выполнения.
Пример кода
# CMakeLists.txt — опциональная цель с покрытием кода
option(ENABLE_COVERAGE "Включить сбор покрытия кода gcov/lcov" OFF)
if (ENABLE_COVERAGE)
add_compile_options(--coverage -O0 -g)
add_link_options(--coverage)
endif()
add_executable(TestCalculator test_calculator.cpp)
target_link_libraries(TestCalculator PRIVATE Qt6::Test)
add_test(NAME TestCalculator COMMAND TestCalculator)
# Сборка с включённым покрытием
cmake -B build-coverage -S . -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build-coverage
# Запуск тестов
cd build-coverage
ctest
# Сбор данных покрытия с помощью lcov
lcov --capture --directory . --output-file coverage.info
# Исключение системных и сторонних заголовков из отчёта
lcov --remove coverage.info '/usr/*' '*/build-coverage/*' --output-file coverage_filtered.info
# Генерация HTML-отчёта
genhtml coverage_filtered.info --output-directory coverage_html
# Открыть coverage_html/index.html в браузере для просмотра отчёта
Пояснения к коду
Опция ENABLE_COVERAGE делает сбор покрытия опциональным — обычная сборка для разработки не несёт накладных расходов инструментации. При включении добавляются флаги --coverage (для компилятора и линкера) и -O0 -g — отключение оптимизаций важно, поскольку агрессивная оптимизация компилятора может слить или удалить строки кода, что искажает построчное соответствие отчёта покрытия исходному коду.
После запуска тестов lcov --capture агрегирует все .gcda-файлы в единый файл coverage.info, который затем фильтруется командой --remove для исключения системных заголовков и самих тестовых файлов из отчёта (покрытие тестов самих себя неинформативно), а genhtml превращает итоговые данные в навигируемый HTML-отчёт с построчной подсветкой.
Подводные камни
- Включение покрытия в Release-сборке с оптимизациями. Оптимизирующий компилятор может объединять или переупорядочивать инструкции, из-за чего соответствие между строками исходного кода и фактически выполненными инструкциями становится неточным — покрытие почти всегда измеряют на Debug-сборке без оптимизаций.
- Интерпретация 100% покрытия как доказательства отсутствия багов. Покрытие показывает, что строка была выполнена, но не то, что результат был корректно проверен — тест без единой проверки (
QCOMPARE/QVERIFY) может дать высокое покрытие, не находя при этом ни одной реальной ошибки. - Покрытие генерируемого кода (moc, uic) искажает общую цифру. Сгенерированные Qt файлы (
moc_*.cpp,ui_*.h) обычно не несут смысловой нагрузки для анализа покрытия и должны исключаться из отчёта аналогично системным заголовкам — иначе общий процент покрытия проекта может казаться искусственно ниже или выше реального. - Несовпадение версии gcov с версией компилятора GCC.
gcovтесно связан с конкретной версией GCC, которой был скомпилирован код — попытка обработать данные покрытия инструментом другой версии может приводить к ошибкам или неверным данным в отчёте.