Покрытие кода тестами в C++: от Gcov до метрик качества

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему "зелёные" тесты — это ещё не победа?

Представьте: вы запускаете тысячу unit-тестов, все они проходят с зелёной галочкой, CI-пайплайн счастлив, менеджер доволен. Но проходит неделя, и в production падает критический баг. Знакомая ситуация? Чаще всего проблема в том, что тесты существуют, но они не проверяют реально исполняемый код. Они могут тестировать только 10% исходников, а остальные 90% — "серая зона", где прячутся ошибки.

Именно здесь на сцену выходит покрытие кода тестами в C++ (code coverage). Это не просто модная метрика для отчёта перед заказчиком. Это ваш рентгеновский аппарат, который показывает: какие строки кода реально выполняются во время тестов, а какие — никогда не запускались. Сегодня мы разберём, как правильно настроить инструменты покрытия на C++, какие метрики действительно важны, и как не утонуть в ложных срабатываниях.

Что такое покрытие кода и зачем оно нужно C++ разработчику?

Покрытие кода (code coverage) — это метрика, показывающая, какой процент исходного кода вашей программы был выполнен во время прогона тестов. В C++ это особенно критично, потому что язык позволяет писать низкоуровневый код, оптимизации и шаблонную магию, которая может "выстрелить" в самый неожиданный момент.

Основные виды метрик покрытия

Не все метрики одинаково полезны. Вот что реально имеет значение:

  • Line Coverage (покрытие строк) — самая базовая метрика. Сколько строк исходника было затронуто. Легко измерить, но легко обмануть.
  • Branch Coverage (покрытие ветвлений) — проверяет, были ли пройдены обе ветки каждого if/else или switch. Гораздо честнее, чем line coverage.
  • Function Coverage (покрытие функций) — вызывалась ли функция хотя бы раз. Полезно для поиска "мёртвого" кода.
  • Condition Coverage (покрытие условий) — проверяет все подвыражения в сложных логических операторах (&&, ||). Самый строгий, но и самый ресурсоёмкий.

Для обычного проекта C++ хорошим тоном считается цель в 70-80% line coverage и не менее 60% branch coverage. 100% покрытие — это утопия, но стремиться к 90% в критических модулях (работа с памятью, сетевые протоколы) — задача вполне реальная.

Инструменты для анализа покрытия C++ кода

Экосистема C++ предлагает несколько мощных инструментов. Мы рассмотрим топ-3, которые реально используются в продакшене.

1. Gcov — классика от GCC

gcov — это встроенный инструмент в компилятор GCC. Он работает на уровне машинного кода, поэтому даёт очень точные результаты. Основные плюсы: бесплатно, открыто, работает с CMake и Makefile.

Как запустить:

// Компилируем с флагами покрытия
g++ -fprofile-arcs -ftest-coverage -O0 main.cpp -o test_app

// Запускаем тесты
./test_app

// Генерируем отчёт
gcov main.cpp

После этого вы получите файлы *.gcov, где будет показано, сколько раз выполнялась каждая строка. Минус: формат вывода — текстовый, не очень наглядный.

2. LCOV — графический интерфейс для Gcov

lcov — это надстройка над gcov, которая генерирует красивые HTML-отчёты. Вы сможете видеть зелёные (покрытые) и красные (непокрытые) строки прямо в браузере.

# Собираем данные покрытия
lcov --capture --directory . --output-file coverage.info

# Генерируем HTML
genhtml coverage.info --output-directory coverage_report

Это стандарт индустрии для CI/CD. Jenkins, GitLab CI, GitHub Actions — везде можно настроить автоматическую генерацию LCOV.

3. Clang Sanitizer Coverage (Source-based)

Если вы используете Clang, у вас есть доступ к -fsanitize-coverage. Этот инструмент работает на уровне AST (абстрактного синтаксического дерева), что позволяет получать более точные данные о покрытии ветвлений и условий, чем gcov.

// Компилируем с Clang
clang++ -fsanitize-coverage=trace-pc-guard -fsanitize=address main.cpp -o test_app

// После запуска тестов данные собираются через специальные callback-функции

Идеально подходит для фаззинга и сложных проектов, где важна каждая микросекунда.

Пример кода: как выглядит анализ покрытия на практике

Допустим, у нас есть простой модуль для работы с числами Фибоначчи:

// fibonacci.h
#pragma once
int fibonacci(int n);

// fibonacci.cpp
#include "fibonacci.h"

int fibonacci(int n) {
    if (n <= 1) {
        return n;  // строка 6
    }
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; ++i) {
        c = a + b;  // строка 10
        a = b;
        b = c;
    }
    return b;  // строка 14
}

Напишем тест (используем Google Test):

// test_fibonacci.cpp
#include <gtest/gtest.h>
#include "fibonacci.h"

TEST(FibonacciTest, HandlesZero) {
    EXPECT_EQ(fibonacci(0), 0);
}

TEST(FibonacciTest, HandlesPositive) {
    EXPECT_EQ(fibonacci(10), 55);
}

Компилируем с поддержкой покрытия, запускаем тесты, затем lcov. Отчёт покажет:

  • Строка 6 (return n) — покрыта (вызывалась при n=0).
  • Строка 10 (c = a + b) — покрыта.
  • Строка 14 (return b) — покрыта.

Но если мы не написали тест для n=1, то ветка if (n <= 1) при n=1 не будет проверена, хотя строка 6 формально покрыта. Вот почему branch coverage важнее line coverage.

Метрики, которые стоит отслеживать в CI

Чтобы не превратить анализ покрытия в бюрократию, сосредоточьтесь на трёх ключевых показателях:

  1. Общий процент покрытия (Line + Branch) — должен быть не ниже порога, заданного в вашем проекте. Обычно 70%.

Блоги

Рекомендаций по книгам