Чому метрика code coverage бреше в CI/CD і ламає релізи
Метрика code coverage виглядає об'єктивною рівно до моменту, коли її зіставляють з історією падінь. Команда тримає поріг покриття вище 80 %, збірка зелена, а регресії в релізі приходять стабільно.
Це не парадокс: звіт про покриття показує, які рядки виконалися хоча б раз, але мовчить про те, чи перевіряли їх на різних даних і з різними станами системи.
Перше джерело брехні — непокриті вітки при формальному виконанні рядків. Рядок з умовою зараховується як пройдений, якщо код дійшов до неї, а обробка помилки, порожня відповідь зовнішнього сервісу або відмова з'єднання залишаються незайманими.
Реліз іде далі з упевненим «покриття зросло», а падає на першому нештатному сценарії — тому самому, заради якого цю вітку й написано.
Другий — тести заради числа. Коли поріг покриття перетворюється на звітний показник, з'являються перевірки, які викликають функцію і переконуються у факті виклику, а не в поведінці.
Формально якість тестів зростає, реально — зростає обсяг коду, який ніхто не перевіряв по суті, і ціна підтримки такого набору перевищує користь від нього.
Третій — застарілі винятки. Файл одного разу потрапив до списку ігнорованих: згенерований код, разовий адаптер, ділянка під міграцію. Виняток живе в конфігурації безстроково, ділянка змінюється, а метрика за нею не рахується — у звіті дірка, про яку ніхто не пам'ятає.
Четвертий — відсутність історії прогонів. Поріг перевіряється на кожному коміті, але тренду немає: покриття тихо сповзає з 84 до 76 % за десяток змін, кожна з яких формально проходить допуск.
Нижче розбираємо, як це закривається: що рахувати, де ставити пороги і як ловити регресії, а не цифру.
Що дає керування покриттям: вимірювані ефекти
Поріг за змінами замість загального відсотка. Перевірка дивиться лише на рядки, які прийшли в поточних змінах: якщо покриття нових ділянок нижче узгодженої межі, злиття зупиняється до рев'ю, а не після.
Такий поріг не блокує роботу над наявним кодом і не вимагає героїчних зусиль із підняття загального відсотка — зниження кількості регресій досягається точково.
Тренд покриття як керована метрика. Значення зберігаються за гілками й релізами, тому видно не «скільки зараз», а куди рухається зростання покриття: який модуль просів після переписування, де нові тести не встигають за кодом. Рішення про блокування ухвалюється за динамікою за кілька тижнів, а не за разовим зрізом.
Час розбору проваленого прогону. Звіт надходить із розбивкою за файлами й посиланням на непокриті рядки прямо в завданні на збірку — не потрібно вивантажувати артефакт і зіставляти його з логами вручну. Алерт у канал команди містить гілку, конкретну ділянку й автора змін, тому розбір займає хвилини.
Передбачуваність злиттів. Ті самі правила діють для всіх гілок і для локального прогону, тож результат перевірки відомий ще до надсилання змін.
Рев'юер обговорює логіку, а не сперечається про покриття, і черга злиттів перестає залежати від того, хто саме запустив перевірку і з якими налаштуваннями.
Підходи до контролю покриття: порівняння й обмеження
Перевірка покриття рідко тримається на одному числі. Загальний поріг обходиться сам собою: новий модуль без тестів розмиває середній відсоток, а падіння на десяті частки блокує реліз не по ділу — команда починає глушити перевірку або опускати поріг. Тому поряд живуть кілька правил: жорсткий поріг за зміненими рядками, м'який загальний і тренд.
Рахувати всі метрики з одного прогону вміє SonarQube, тож вибір не в тому, що вимірювати, а в тому, яке правило робить блокувальну перевірку.
| Підхід | Коли підходить | Слабкі місця |
|---|---|---|
| Загальний поріг покриття | Рівна база, метрика змінюється повільно | Новий файл без тестів розбавляє відсоток; поріг доводиться знижувати |
| Поріг за зміненими рядками | Активна розробка, часті правки | Не бачить деградацію в старому коді, який не чіпають |
| Трендовий контроль | Великі репозиторії, де поріг уперся в стелю | Потрібна накопичена історія замірів, реагує із затримкою |
| Режим спостереження | Впровадження перевірки на наявному коді | Нічого не блокує: регрес проходить, поки правило не перемкнули |
Робоча комбінація — поріг за зміненими рядками як блокувальна перевірка, тренд як сигнал про повільне сповзання, загальний поріг як фон. Правила винятків оформлюють явно й вузько: згенерований код, міграції, конфігурація. Інакше винятки розростаються і перевірка втрачає сенс.
Впровадження: від аудиту конвеєра до алертів за покриттям
Впровадження покриття — не встановлення інструмента, а перебудова конвеєра: збірка, тести, звіти й перевірки на злиття починають працювати як одна система.
Порядок робіт нижче влаштовано так, що кожен етап спирається на дані попереднього — інакше пороги перетворюються на формальність, яку команда вимикає за першого незручного спрацювання.
- Аудит конвеєра. Розбираємо наявні кроки збірки й тестів, дивимося, які звіти формуються і де дані про покриття губляться між етапами. Результат — карта: обов'язкові перевірки, дубльовані прогони, ділянки без публікації звіту.
- Збір метрик покриття. Зводимо всі прогони до єдиної схеми: звіт у загальному форматі, прив'язка до конкретної зміни й гілки, завантаження в сховище історії. Покриття перестає бути виводом у лозі й стає даними за кожним модулем.
- Історія прогонів. Накопичена база дає порівняння: видно, де покриття справді падає, а де коливається через нестабільний тест. За трендом вибираємо модулі з найбільшим приростом від точкового доопрацювання тестів.
- Пороги й оповіщення. Два рівні — загальний за кодовою базою і за новим кодом у зміні, для критичних модулів жорсткіше. Оповіщення про падіння йде в робочий канал команди з переліком файлів і автором зміни, щоб розбір ішов без пошуку причини.
- Перевірка на реальних злиттях. Прогоняємо на справжніх змінах, ловимо хибні спрацювання, задаємо винятки для згенерованого коду, міграцій і самих тестів. Лише після цього перевірка стає блокувальною.
- Регламент перевірок. Передаємо документ: як змінювати поріг, хто погоджує виняток, що робить черговий при оповіщенні. Плюс розбір із командою — щоб правило жило, а не вимикалося.
Після передачі регламенту система обслуговує себе сама: пороги змінюються за процедурою, історія накопичується, падіння покриття видно в день зміни, а не перед релізом.
Що передаємо на руки: правила, конфігурації, регламенти
Передача на руки — це момент, коли пороги й винятки команда замовника змінює сама, не розбираючи заново, як влаштовано збір. Тому крім працюючого конвеєра віддаємо набір документів, за якими рішення про покриття можна ухвалити без наших інженерів.
- Файл правил покриття — де оголошені охоплення, формат звіту й точки злиття даних із модульних та інтеграційних прогонів.
- Політика винятків — перелік шляхів і типів коду, виведених із підрахунку, з обов'язковим обґрунтуванням і датою перегляду в кожного запису.
- Шаблон звіту — зрізи за модулями, порівняння з попереднім релізом і тренд за останні прогони, а не єдина цифра.
- Схема оповіщень — кому й куди йде сигнал при просадці, з розділенням на попередження й блокування злиття.
- Інструкція черговому — порядок розбору падіння: перевірити нові винятки, тести, що впали, гілку з незакритими ділянками.
- Регламент перегляду порогів — хто і за якою подією піднімає планку, щоб вона не залишилася на рівні першого запуску.
- Карта володіння — яка команда відповідає за покриття якого модуля, щоб питання щодо винятку йшло за адресою, а не до нас.
Перевіряємо передачу просто: на тестовій гілці змінюємо поріг і додаємо виняток за інструкцією, не залучаючи наших інженерів. Якщо вийшло з першого разу — документи робочі, а не декоративні.
Кейс: пороги й тренди прибрали регресії з релізів
До нас прийшла продуктова команда: десяток сервісів в одному репозиторії, релізи двічі на тиждень, загальне покриття близько 70% і повна довіра до цієї цифри.
Проблема була в тому, що середній показник не відображав ризик — правки в платіжному й білінговому модулях ішли в прод без тестів на тих рядках, які змінювалися.
За квартал команда ловила по три-чотири регресії на реліз, а розбір із ручною збіркою попередньої версії займав від шести до десяти годин і вимагав двох інженерів.
Ми перенесли акцент із загального відсотка на покриття змінених рядків: у кожному запиті на злиття окремо рахувалося, яка частка доданих і змінених рядків закрита тестами.
Порогове правило у SonarQube пов'язали з обов'язковим статусом перевірки — для платіжного й білінгового модулів збірка падає, якщо змінений рядок не покритий, для інших гілок поріг м'якший.
Поруч побудували тижневий тренд і алерти на просадку, щоб якість було видно до релізу, а не після інциденту.
Метрики до і після
За три місяці покриття змінених рядків зросло з 46% до 84%, при цьому загальний відсоток майже не рухався — він і не був метою.
Регресій на реліз стало одна на п'ять-шість випусків замість трьох-чотирьох, час відновлення скоротився з шести-десяти годин до півтори: відкат запускається однією командою за міткою попереднього релізу. Збірка подорожчала на дві-три хвилини — розмін, який команда прийняла свідомо.
Чим відрізняється code coverage за рядками й за вітками?
Покриття рядків відповідає на питання «чи дійде виконання до цього рядка», покриття віток — «чи перевірені всі результати кожного рішення». Рядкове покриття закриває рядок, якщо виконалася хоча б одна його інструкція.
Покриття віток вимагає пройти обидві сторони умови — істину й хибу — включно з тернарними операторами, вітками switch і коротким замиканням у логічних виразах.
Розрив між метриками і є джерелом хибного покриття. Рядок із if (a && b) стає «зеленим», щойно a виявиться хибним: права частина не виконається жодного разу, а звіт покаже 100%.
Так само поводять себе ранні повернення й обробники винятків — рядки закриті, а сценарій помилки не перевірений.
| Метрика | Що показує | Де бреше | Роль у гейті |
|---|---|---|---|
| Рядки | частку виконаних рядків | коротке замикання, ранні повернення | тренд за проєктом |
| Вітки | усі результати рішень | складні булеві вирази | поріг на зміненому коді |
| Умови | кожен операнд окремо | рідко підтримується повністю | точково, критичні модулі |
| Функції | сам факт виклику | якості перевірок не видно | сигнал «модуль забутий» |
Основною метрикою беремо покриття віток: інструменти на кшталт JaCoCo рахують його з того самого прогону, що й рядки, без додаткових тестів. Рядкове покриття залишаємо як індикатор тренду й невикористаного коду.
Поріг за вітками ставимо лише на зміненому коді — інакше легасі-модулі блокують будь-який реліз, а умови й функції тримаємо допоміжними метриками.
Часті сумніви інженерів перед налаштуванням покриття
Сумніви перед введенням порогів зазвичай технічні, а не організаційні: інженери вже бачили метрику, яка заважала більше, ніж допомагала. Нижче — ситуації, які спливають майже на кожному впровадженні, і те, як вони вирішуються конфігурацією, а не умовляннями.
Прогін стане вдвічі довшим — збір покриття гальмує тести?
Інструментування кожного тесту справді дороге. Збір метрик виносять на виділену конфігурацію: один прогін з інструментами віддає звіт, решта завдань у матриці йдуть без них. Кеш залежностей і паралельні виконавці повертають час збірки до попереднього.
Покриття малюється порожніми тестами — навіщо тоді поріг?
За рядками — малюється, тому поріг рахують за зміненими рядками в запиті на злиття, а не за всім репозиторієм. Додатково фіксують частку тестів із перевірками тверджень: тест без твердження рядок не закриває. Така пара метрик накручується в рази важче.
У нас моноліт і старий код — репозиторій не дотягне до загального порогу.
Загальний поріг і не потрібен. Базовий рівень фіксує поточний стан, а правило «не гірше, ніж було» застосовується до змінених файлів. Звіти зводять в одне сховище з прив'язкою до коміту, тренд будується за гілками, алерти спрацьовують на падіння щодо базової гілки.
Під час розпилювання моноліту планку піднімають за модулями, а винятки для успадкованого коду задають за шляхами з датою перегляду.
Обговорімо задачу: як налаштувати пороги й оповіщення
Налаштувати пороги й оповіщення «за замовчуванням» не можна: той самий відсоток покриття означає різне для модуля розрахунків і для коду, який просто проксіює зовнішній виклик.
Тому старт робіт починаємо з розбору того, що вже збирається: які звіти формуються, за якими гілками, де перевірка вже стоїть блокувальною, а де її просто ігнорують. Далі фіксуємо, за якими модулями відмова дорожча за все — за ними поріг буде жорсткішим, ніж за рештою коду.
Щоб повернутися з оцінкою й планом впровадження, а не із загальними міркуваннями, нам зазвичай достатньо:
- доступ до конвеєра й права на зміну конфігурації збірки — дивимося реальні кроки, а не переказ;
- звіти про покриття за останні збірки й за основною гілкою — з них беремо базу для порогу, який не опускається нижче досягнутого рівня;
- список критичних модулів і сервісів — за ними ставимо жорсткий поріг на весь код, а не лише на змінений;
- перелік гілок і умов злиття — визначаємо, де перевірка стає обов'язковою;
- очікування щодо режиму перевірок: блокувати злиття чи спершу лише попереджати й збирати статистику;
- контакт чергового зі збірок — щоб розбирати хибні спрацювання одразу, а не накопичувати їх у ігнорі;
- точки, де рішення за вами: з якого рівня починаємо і як швидко піднімаємо планку за репозиторіями.
Опишіть задачу — повернемося з оцінкою й планом робіт.







