Настройка Code Coverage в CI/CD: пороги, тренды и алерты

Пороги покрытия, которые защищают релизы, а не тормозят их, — наша задача. Мы настраиваем контроль code coverage в вашем конвейере под ключ: собираем метрики, задаём пороги по критичным модулям, включаем оповещения о регрессиях. Команда перестаёт спорить о цифрах и тратит меньше времени на разбор сломанных сборок.
Часто задаваемые вопросы

Почему метрика code coverage врёт в CI/CD и ломает релизы

Метрика code coverage выглядит объективной ровно до момента, когда её сопоставляют с историей падений. Команда держит порог покрытия выше 80 %, сборка зелёная, а регрессии в релизе приходят стабильно. Это не парадокс: отчёт о покрытии показывает, какие строки исполнились хотя бы раз, но молчит о том, проверяли ли их на разных данных и с разными состояниями системы.

Первый источник вранья — непокрытые ветви при формальном выполнении строк. Строка с условием засчитывается как пройденная, если код дошёл до неё, а обработка ошибки, пустой ответ внешнего сервиса или отказ соединения остаются нетронутыми. Релиз уходит дальше с уверенным «покрытие выросло», а падает на первом нештатном сценарии — том самом, ради которого эта ветвь и написана.

Второй — тесты ради числа. Когда порог покрытия превращается в отчётный показатель, появляются проверки, которые вызывают функцию и убеждаются в факте вызова, а не в поведении. Формально качество тестов растёт, реально — растёт объём кода, который никто не проверял по существу, и цена поддержки такого набора превышает пользу от него.

Третий — устаревшие исключения. Файл однажды попал в список игнорируемых: сгенерированный код, разовый адаптер, участок под миграцию. Исключение живёт в конфигурации бессрочно, участок меняется, а метрика по нему не считается — в отчёте дырка, о которой никто не помнит.

Четвёртый — отсутствие истории прогонов. Порог проверяется на каждом коммите, но тренда нет: покрытие тихо сползает с 84 до 76 % за десяток изменений, каждое из которых формально проходит допуск. Ниже разбираем, как это закрывается: что считать, где ставить пороги и как ловить регрессии, а не цифру.

Что даёт управление покрытием: измеримые эффекты

Порог по изменениям вместо общего процента. Проверка смотрит только на строки, которые пришли в текущих изменениях: если покрытие новых участков ниже согласованной границы, слияние останавливается до ревью, а не после. Такой порог не блокирует работу над существующим кодом и не требует героических усилий по поднятию общего процента — снижение числа регрессий достигается точечно.

Тренд покрытия как управляемая метрика. Значения хранятся по веткам и релизам, поэтому видно не «сколько сейчас», а куда движется рост покрытия: какой модуль просел после переписывания, где новые тесты не успевают за кодом. Решение о блокировке принимается по динамике за несколько недель, а не по разовому срезу.

Время разбора упавшего прогона. Отчёт приходит с разбивкой по файлам и ссылкой на непокрытые строки прямо в задании на сборку — не нужно выкачивать артефакт и сопоставлять его с логами вручную. Алерт в канал команды содержит ветку, конкретный участок и автора изменений, поэтому разбор занимает минуты.

Предсказуемость слияний. Одни и те же правила действуют для всех веток и для локального прогона, так что результат проверки известен ещё до отправки изменений. Ревьюер обсуждает логику, а не спорит о покрытии, и очередь слияний перестаёт зависеть от того, кто именно запустил проверку и с какими настройками.

Подходы к контролю покрытия: сравнение и ограничения

Проверка покрытия редко держится на одном числе. Общий порог обходится сам собой: новый модуль без тестов размывает средний процент, а падение на десятые доли блокирует релиз не по делу — команда начинает глушить проверку или опускать порог. Поэтому рядом живут несколько правил: жёсткий порог по изменённым строкам, мягкий общий и тренд.

Считать все метрики из одного прогона умеет SonarQube, так что выбор не в том, что измерять, а в том, какое правило делает блокирующую проверку.

Подход Когда подходит Слабые места
Общий порог покрытия Ровная база, метрика меняется медленно Новый файл без тестов разбавляет процент; порог приходится снижать
Порог по изменённым строкам Активная разработка, частые правки Не видит деградацию в старом коде, который не трогают
Трендовый контроль Крупные репозитории, где порог упёрся в потолок Нужна накопленная история замеров, реагирует с задержкой
Режим наблюдения Внедрение проверки на существующем коде Ничего не блокирует: регресс проходит, пока правило не переключили

Рабочая комбинация — порог по изменённым строкам как блокирующая проверка, тренд как сигнал о медленном сползании, общий порог как фон. Правила исключений оформляют явно и узко: сгенерированный код, миграции, конфигурация. Иначе исключения разрастаются и проверка теряет смысл.

Внедрение: от аудита конвейера до алертов по покрытию

Внедрение покрытия — не установка инструмента, а перестройка конвейера: сборка, тесты, отчёты и проверки на слияние начинают работать как одна система. Порядок работ ниже устроен так, что каждый этап опирается на данные предыдущего — иначе пороги превращаются в формальность, которую команда отключает при первом неудобном срабатывании.

  1. Аудит конвейера. Разбираем существующие шаги сборки и тестов, смотрим, какие отчёты формируются и где данные о покрытии теряются между этапами. Результат — карта: обязательные проверки, дублирующиеся прогоны, участки без публикации отчёта.
  2. Сбор метрик покрытия. Сводим все прогоны к единой схеме: отчёт в общем формате, привязка к конкретному изменению и ветке, загрузка в хранилище истории. Покрытие перестаёт быть выводом в логе и становится данными по каждому модулю.
  3. История прогонов. Накопленная база даёт сравнение: видно, где покрытие действительно падает, а где колеблется из-за нестабильного теста. По тренду выбираем модули с наибольшим приростом от точечной доработки тестов.
  4. Пороги и оповещения. Два уровня — общий по кодовой базе и по новому коду в изменении, для критичных модулей жёстче. Оповещение о падении уходит в рабочий канал команды с перечнем файлов и автором изменения, чтобы разбор шёл без поиска причины.
  5. Проверка на реальных слияниях. Прогоняем на настоящих изменениях, ловим ложные срабатывания, задаём исключения для сгенерированного кода, миграций и самих тестов. Только после этого проверка становится блокирующей.
  6. Регламент проверок. Передаём документ: как менять порог, кто согласует исключение, что делает дежурный при оповещении. Плюс разбор с командой — чтобы правило жило, а не отключалось.

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

Что передаём на руки: правила, конфигурации, регламенты

Передача на руки — это момент, когда пороги и исключения команда заказчика меняет сама, не разбирая заново, как устроен сбор. Поэтому кроме работающего конвейера отдаём набор документов, по которым решение о покрытии можно принять без наших инженеров.

  • Файл правил покрытия — где объявлены охваты, формат отчёта и точки слияния данных из модульных и интеграционных прогонов.
  • Политика исключений — перечень путей и типов кода, выведенных из подсчёта, с обязательным обоснованием и датой пересмотра у каждой записи.
  • Шаблон отчёта — срезы по модулям, сравнение с предыдущим релизом и тренд за последние прогоны, а не единственная цифра.
  • Схема оповещений — кому и куда уходит сигнал при просадке, с разделением на предупреждение и блокировку слияния.
  • Инструкция дежурному — порядок разбора падения: проверить новые исключения, упавшие тесты, ветку с незакрытыми участками.
  • Регламент пересмотра порогов — кто и по какому событию поднимает планку, чтобы она не осталась на уровне первого запуска.
  • Карта владения — какая команда отвечает за покрытие какого модуля, чтобы вопрос по исключению шёл по адресу, а не к нам.

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

Кейс: пороги и тренды убрали регрессии из релизов

К нам пришла продуктовая команда: десяток сервисов в одном репозитории, релизы дважды в неделю, общее покрытие около 70% и полное доверие к этой цифре. Проблема была в том, что средний показатель не отражал риск — правки в платёжном и биллинговом модулях уходили в прод без тестов на тех строках, которые менялись. За квартал команда ловила по три-четыре регрессии на релиз, а разбор с ручной сборкой прежней версии занимал от шести до десяти часов и требовал двух инженеров.

Мы перенесли акцент с общего процента на покрытие изменённых строк: в каждом запросе на слияние отдельно считалось, какая доля добавленных и изменённых строк закрыта тестами. Пороговое правило в SonarQube связали с обязательным статусом проверки — для платёжного и биллингового модулей сборка падает, если изменённая строка не покрыта, для остальных веток порог мягче. Рядом построили недельный тренд и алерты на просадку, чтобы качество было видно до релиза, а не после инцидента.

Метрики до и после

За три месяца покрытие изменённых строк выросло с 46% до 84%, при этом общий процент почти не двигался — он и не был целью. Регрессий на релиз стало одна на пять-шесть выпусков вместо трёх-четырёх, время восстановления сократилось с шести-десяти часов до полутора: откат запускается одной командой по метке предыдущего релиза. Сборка подорожала на две-три минуты — размен, который команда приняла сознательно.

Чем отличается code coverage по строкам и по ветвям?

Покрытие строк отвечает на вопрос «дойдёт ли исполнение до этой строки», покрытие ветвей — «проверены ли все исходы каждого решения». Строковое покрытие закрывает строку, если выполнилась хотя бы одна её инструкция. Покрытие ветвей требует пройти обе стороны условия — истину и ложь — включая тернарные операторы, ветки switch и короткое замыкание в логических выражениях.

Разрыв между метриками и есть источник ложного покрытия. Строка с if (a && b) становится «зелёной», как только a окажется ложным: правая часть не выполнится ни разу, а отчёт покажет 100%. Так же ведут себя ранние возвраты и обработчики исключений — строки закрыты, а сценарий ошибки не проверен.

Метрика Что показывает Где врёт Роль в гейте
Строки долю исполненных строк короткое замыкание, ранние возвраты тренд по проекту
Ветви все исходы решений сложные булевы выражения порог на изменённом коде
Условия каждый операнд отдельно редко поддерживается целиком точечно, критичные модули
Функции сам факт вызова качества проверок не видно сигнал «модуль забыт»

Основной метрикой берём покрытие ветвей: инструменты вроде JaCoCo считают её из того же прогона, что и строки, без дополнительных тестов. Строковое покрытие оставляем как индикатор тренда и неиспользуемого кода. Порог по ветвям ставим только на изменённом коде — иначе легаси-модули блокируют любой релиз, а условия и функции держим вспомогательными метриками.

Частые сомнения инженеров перед настройкой покрытия

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

Прогон станет вдвое дольше — сбор покрытия тормозит тесты?

Инструментирование каждого теста действительно дорого. Сбор метрик выносят на выделенную конфигурацию: один прогон с инструментами отдаёт отчёт, остальные задачи в матрице идут без них. Кэш зависимостей и параллельные исполнители возвращают время сборки к прежнему.

Покрытие рисуется пустыми тестами — зачем тогда порог?

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

У нас монолит и старый код — репозиторий не дотянет до общего порога.

Общий порог и не нужен. Базовый уровень фиксирует текущее состояние, а правило «не хуже, чем было» применяется к изменённым файлам. Отчёты сводят в одно хранилище с привязкой к коммиту, тренд строится по веткам, алерты срабатывают на падение относительно базовой ветки. При распиле монолита планку поднимают по модулям, а исключения для унаследованного кода задают по путям с датой пересмотра.

Обсудим задачу: как настроить пороги и оповещения

Настроить пороги и оповещения «по умолчанию» нельзя: один и тот же процент покрытия значит разное для модуля расчётов и для кода, который просто проксирует внешний вызов. Поэтому старт работ начинаем с разбора того, что уже собирается: какие отчёты формируются, по каким веткам, где проверка уже стоит блокирующей, а где её просто игнорируют. Дальше фиксируем, по каким модулям отказ дороже всего — по ним порог будет жёстче, чем по остальному коду.

Чтобы вернуться с оценкой и планом внедрения, а не с общими соображениями, нам обычно достаточно:

  • доступ к конвейеру и права на изменение конфигурации сборки — смотрим реальные шаги, а не пересказ;
  • отчёты о покрытии за последние сборки и по основной ветке — из них берём базу для порога, который не опускается ниже достигнутого уровня;
  • список критичных модулей и сервисов — по ним ставим жёсткий порог на весь код, а не только на изменённый;
  • перечень веток и условий слияния — определяем, где проверка становится обязательной;
  • ожидания по режиму проверок: блокировать слияние или сперва только предупреждать и собирать статистику;
  • контакт дежурного по сборкам — чтобы разбирать ложные срабатывания сразу, а не копить их в игноре;
  • точки, где решение за вами: с какого уровня начинаем и как быстро поднимаем планку по репозиториям.

Опишите задачу — вернёмся с оценкой и планом работ.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    815
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139