Білоскринькові техніки: покриття коду
Зміст
У pull request автотестів зʼявляється коментар від CI: «coverage впав з 84% до 81%, мердж заблоковано». Що саме впало? Чи означають ці 84%, що 84% поведінки продукту перевірено? І чому розробник поруч каже, що їхні 100% на модулі оплати «нічого не гарантують»? Без розуміння метрик покриття коду ці розмови звучать як шаманство — а на співбесідах рівня middle питання «чим statement coverage відрізняється від branch coverage» стало класикою, бо за дві хвилини показує, чи розуміє кандидат, що вимірює метрика, якою всі звітують.
Це глава-поглиблення: при першому проході розділу її можна пропустити — базовий трек від класів еквівалентності до вибору технік самодостатній без неї. Повертайтеся сюди, коли почнете писати автотести або коли у вашій команді coverage стане критерієм якості пайплайна. Водночас це єдина глава на сайті, де тема покриття коду розібрана повністю, — решта глав лише посилаються сюди.
Що таке білоскринькові техніки
Усі техніки попередніх глав — специфікаційні (black-box): тест-базисом служать вимоги, а код — чорна скринька. Білоскринькові, або структурні, техніки (white-box, structure-based) перевертають картину: тест-базис — сам код, його внутрішня структура. Питання «що перевірити?» перетворюється на «які частини коду мої тести реально виконали, а які — ні» (класифікацію технік ми вводили в главі «Тест-дизайн: від вимоги до перевірки»).
Звідси два практичні застосування:
- Дизайн тестів: дивимось на код і додаємо тести, поки не виконається кожен оператор, кожна гілка — залежно від обраного рівня.
- Вимірювання: запускаємо наявні тести (спроєктовані будь-якою технікою) під інструментом покриття й дивимося, що вони зачепили. Саме так coverage найчастіше живе в командах — як метрика, а не як техніка дизайну.
Кожен рівень покриття відповідає на питання «виконано чи ні» для своєї одиниці вимірювання: оператор, гілка, умова, шлях. Що дрібніша одиниця — то сильніша гарантія і то дорожчий набір тестів.
Покриття операторів (statement coverage)
Найслабша і найпростіша метрика: частка виконуваних операторів (statements) коду, які тести виконали хоча б раз.
statement coverage = виконані оператори / усі виконувані оператори × 100%
Візьмімо функцію знижки:
function applyDiscount(price: number, isVip: boolean): number {
let total = price; // оператор 1
if (isVip) { // оператор 2
total = total * 0.9; // оператор 3
}
return total; // оператор 4
}
Один тест applyDiscount(100, true) виконує всі чотири оператори — 100% statement coverage досягнуто одним тестом. Звучить як перемога, але зверніть увагу: сценарій «не VIP» ми не запускали взагалі. Якби хтось зламав код так, що знижка застосовується всім, — тест лишився б зеленим.
Покриття гілок (branch coverage) і чому 100% statement ≠ 100% branch
Покриття гілок (branch coverage) вимагає, щоб кожне рішення в коді відпрацювало в обидва боки: кожен if — і true, і false; кожна умова циклу — і «зайшли», і «пропустили». На практиці його ототожнюють з покриттям рішень (decision coverage): формально ISTQB розрізняє ці метрики, але на рівні 100% вони збігаються. Одиниця вимірювання — не оператор, а ребро на графі потоку керування (control flow graph).
Подивимось на applyDiscount як на граф:
Тест applyDiscount(100, true) проходить маршрут A → B → C → D. Усі вузли (оператори) відвідано — 100% statement. Але ребро B → D, гілка false, лишилося непокритим: branch coverage — 50%. Ось і вся відповідь на класичне співбесідне «чому 100% statement не гарантує 100% branch»: if без else виконує всі оператори на гілці true, а порожня гілка false операторів не має — statement-метриці нема за що зачепитися. Потрібен другий тест, applyDiscount(100, false).
Звідси правило підпорядкування (subsumption): 100% branch coverage автоматично дає 100% statement coverage, а навпаки — ні. Branch — строго сильніша метрика. Саме тому в силабусі ISTQB CTFL 4.0 (розділ 4.3) із білоскринькових технік лишили саме цю пару — statement і branch: перша як мінімальна база, друга як розумний робочий стандарт.
Практична вага для тестувальника: гілка false у if без else — це той самий «негативний сценарій», який ми звикли шукати специфікаційними техніками. Порожня гілка — улюблене місце багів на кшталт «забули обробити відсутність знижки/токена/зʼєднання».
Покриття умов і MC/DC
Рішення бувають складеними. Ускладнимо код:
function freeShipping(isVip: boolean, total: number): boolean {
return isVip && total > 1000;
}
Тут одне рішення (весь вираз), але дві атомарні умови: isVip і total > 1000. Branch coverage вимагає, щоб рішення загалом було і true, і false, — два тести. Але при цьому окрема умова може жодного разу не «сіпнутися»: пара тестів (true, 1500) і (true, 500) дає обидва результати рішення, а умова isVip завжди true — якщо її випадково інвертують, ці тести нічого не помітять.
Рівні, що працюють з умовами:
| Рівень | Вимога | Мінімум тестів для A && B |
|---|---|---|
| Покриття умов (condition coverage) | Кожна атомарна умова була і true, і false | 2 |
| MC/DC (modified condition/decision coverage) | Кожна умова була в обох значеннях і незалежно вплинула на результат рішення | 3 |
| Повне комбінаторне (multiple condition coverage) | Усі комбінації значень умов | 4 |
Пастка condition coverage: тести (true, 500) і (false, 1500) дають кожній умові обидва значення — формально 100% — але рішення обидва рази false. Покриття умов саме по собі не гарантує навіть покриття гілок. (Нюанс реального коду: && у JS/TS обчислюється ліниво — short-circuit, — тож при isVip = false друга умова взагалі не виконується; класична теорія покриття умов цим нехтує, а інструменти на кшталт Istanbul рахують кожен операнд окремо.)
MC/DC закриває цю діру: для кожної умови має існувати пара тестів, де змінюється лише вона — і разом з нею змінюється результат рішення. Для isVip && total > 1000:
(true, 1500)→ true(false, 1500)→ false — порівняно з першим змінився лишеisVip, результат перевернувся:isVipвпливає;(true, 500)→ false — порівняно з першим змінилася лише друга умова, результат перевернувся: вона теж впливає.
Три тести замість чотирьох комбінаторних. Загальне правило: для рішення з n умов MC/DC досяжне щонайменше за n + 1 тестів, тоді як повний перебір коштує 2^n — для восьми умов це 9 тестів проти 256. Саме за цю економію зі збереженням сильної гарантії MC/DC вимагають стандарти критичного ПЗ: авіаційний DO-178C приписує MC/DC для софту найвищого рівня критичності (Level A). У вебі MC/DC руками майже не рахують, але на співбесіді це улюблена перевірка «чи розуміє кандидат різницю між рішенням і умовою».
Ієрархія сили метрик:
Кожна наступна включає гарантії попередньої і коштує дорожче. Окремо від цього ланцюжка стоїть покриття шляхів — про нього далі.
Шляхи і цикломатична складність
Покриття шляхів (path coverage) вимагає виконати кожен маршрут через функцію від входу до виходу. Це найсильніша з метрик потоку керування — і практично недосяжна: три послідовні незалежні if дають 2 × 2 × 2 = 8 шляхів, десять — уже 1024, а цикл із заздалегідь невідомою кількістю ітерацій робить число шляхів практично нескінченним. Тому 100% path coverage — теоретичний орієнтир, а не робоча ціль.
Компромісну відповідь дав Томас Маккейб ще 1976 року: цикломатична складність (cyclomatic complexity) — кількість лінійно незалежних шляхів через код. Для графа потоку керування рахується як E − N + 2 (ребра мінус вузли плюс два), але на практиці простіше: кількість бінарних рішень у коді плюс один. Функція без розгалужень має складність 1; наш applyDiscount з одним if — 2; функція з if, циклом і && в умові — 4.
Чим це корисно QA:
- Оцінка кількості тестів. Цикломатична складність — це верхня межа кількості тестів, потрібних для повного branch coverage, і нижня межа кількості усіх шляхів. Бачите функцію зі складністю 12 — щонайменше стільки ж базисних маршрутів варто мати на увазі, коли оцінюєте повноту тестів.
- Сигнал ризику. Висока складність зазвичай іде в парі з дефектністю і болем супроводу. Лінтери (наприклад, правило
complexityв ESLint) вміють її рахувати; функція-монстр зі складністю 20+ — аргумент і за рефакторинг, і за пріоритетне тестове покриття цього місця. Це прямий місток до ризик-орієнтованого підходу з розділу «Основи тестування»: складність — один з індикаторів «ймовірності» в матриці ризиків.
Як читати coverage-звіт
В екосистемі JS/TS покриття рахують Istanbul (його CLI-обгортка — nyc), c8 поверх вбудованого механізму покриття V8, а тест-ранери на кшталт Jest чи Vitest мають ці інструменти під капотом. Звіт зазвичай показує чотири числа:
| Метрика | Що рахує |
|---|---|
| Statements | Частку виконаних операторів |
| Branches | Частку відпрацьованих гілок рішень |
| Functions | Частку викликаних функцій |
| Lines | Частку виконаних фізичних рядків |
Lines і statements — не синоніми: у рядку if (a) b(); два оператори, і рядкова метрика покаже 100%, коли виконався лише перший. Через це branch — найчесніше з чотирьох чисел: воно падає першим і саме за ним видно недотестовані сценарії.
У HTML-звіті Istanbul непокриті оператори підсвічені червоним, а біля рішень стоять маркери I та E — гілка if або else, яку жоден тест не виконав. Робочий сценарій читання звіту — не «дивитися на відсоток», а спускатися у файли критичних модулів і дивитися, які саме гілки лишилися сірими: часто це обробники помилок, ранні return на невалідних даних і catch-блоки — тобто рівно ті негативні сценарії, які найдорожче пропустити.
Покриття можна зняти й з e2e-прогону: у Playwright є API page.coverage.startJSCoverage() / stopJSCoverage(), який збирає дані про виконаний у браузері код (працює лише в Chromium-браузерах). Так можна побачити, які частини фронтенду ваша e2e-сюїта взагалі не торкається. У CI-пайплайні на покриття вішають пороги — quality gate, що валить білд при падінні нижче заданого відсотка; механіку таких гейтів розбирає розділ «Git і CI/CD».
І головне розмежування: покриття коду відповідає на питання «що з написаного виконано», покриття вимог — «що з обіцяного перевірено». Це різні осі: можна мати 100% по коду і нуль по вимозі, яку забули реалізувати, — коду ж немає, вимірювати нічого. Про другу вісь — глава «Покриття вимог і трасування».
Пастка «100% покриття»
Найважливіше, що треба винести: покриття вимірює виконання, а не перевірку. Тест, який викликає функцію і нічого не assert-ить, дає рівно те саме покриття, що й тест із суворими перевірками. Доведений до абсурду приклад:
test('coverage без перевірки', async () => {
applyDiscount(100, true);
applyDiscount(100, false);
// жодного expect — а branch coverage 100%
});
Метрика зелена, гарантій — нуль. Тому «у нас 95% coverage» саме по собі не каже про якість тестів нічого: це метрика повноти виконання, необхідна, але не достатня умова. Висока цифра корисна навпаки — як детектор дірок: 60% branch у модулі оплати — це факт, що 40% гілок не виконує жоден тест, і ось це вже привід діяти.
Друга половина пастки — Гудхартова: щойно відсоток покриття стає KPI, команда починає оптимізувати відсоток, а не якість. Зʼявляються тести на геттери, тести без асертів, виключення «незручних» файлів зі збору. Здорова практика — тримати поріг як запобіжник від деградації (не даємо новому коду вливатися зовсім без тестів), а рішення про глибину перевірок приймати за ризиком, не за цифрою.
Типові помилки
- Виглядає як «100% statement — усе протестовано», а насправді непокритою може бути половина гілок:
ifбезelseдає повний statement одним позитивним тестом, а негативний сценарій не виконаний жодного разу. - Виглядає як «coverage 90%, тести якісні», а насправді покриття не бачить асертів: сюїта може виконувати код і нічого не перевіряти. Відсоток вимірює виконання, якість перевірок — тільки ревʼю.
- Виглядає як «100% покриття коду = продукт перевірено», а насправді метрика сліпа до відсутнього коду: нереалізована вимога — це нуль рядків, і на відсоток вона не впливає. Пропуски ловить трасування вимог, не coverage.
- Виглядає як «line coverage і statement coverage — одне й те саме», а насправді одиниці різні: у рядку може сидіти кілька операторів, і рядкова метрика систематично оптимістичніша.
- Виглядає як «100% branch = перевірені всі комбінації умов», а насправді branch дивиться лише на результат рішення загалом; вплив кожної атомарної умови гарантує тільки MC/DC, а всі комбінації — multiple condition coverage за ціною 2^n.
Підсумок
- Білоскринькові техніки беруть за тест-базис код: одиниця покриття — оператор, гілка, умова або шлях; що дрібніша одиниця, то сильніша гарантія і дорожчий набір тестів.
- Ієрархія сили: statement ← branch ← MC/DC ← multiple condition; 100% сильнішої метрики гарантує 100% слабшої, навпаки — ні. Класичний доказ —
ifбезelse. - MC/DC вимагає, щоб кожна атомарна умова незалежно перевернула результат рішення: n + 1 тестів замість 2^n — стандарт критичних систем (DO-178C, Level A).
- Цикломатична складність = кількість лінійно незалежних шляхів (рішення + 1): верхня межа тестів для повного branch coverage і сигнал ризику для пріоритизації.
- Покриття вимірює виконання, а не перевірку, і не бачить ненаписаного коду: низький відсоток — це факт і привід діяти, високий — ще не гарантія якості.
Що питають на співбесіді
- «Чим statement coverage відрізняється від branch coverage?» — питання-фільтр. Сильна відповідь обовʼязково містить приклад
ifбезelse: один позитивний тест дає 100% операторів і 50% гілок. Інтервʼюер дивиться, чи кандидат розуміє механізм, а не завчив означення. - «100% покриття коду означає, що багів немає?» — чекають упевненого «ні» з двома причинами: покриття не перевіряє асерти і не бачить нереалізованих вимог. Бонус — згадати, що низьке покриття інформативніше за високе.
- «Що таке MC/DC і навіщо воно, якщо є branch coverage?» — перевірка глибини для middle+. Достатньо пояснити різницю «рішення vs атомарна умова» на прикладі
A && Bі згадати n + 1 проти 2^n; контекст авіоніки — плюс у карму. - «Що таке цикломатична складність і як нею користується QA?» — очікують «кількість незалежних шляхів, рішення плюс один» і практичний кут: оцінка мінімального числа тестів та індикатор ризикових місць коду.
- «У проєкті coverage 85%. Що ви з цим числом зробите?» — питання на зрілість. Слабка відповідь — «підніму до 100%». Сильна — «подивлюся branch-покриття критичних модулів, знайду непокриті гілки обробки помилок, звірю з покриттям вимог і ризиками». Інтервʼюер шукає людину, яка працює зі звітом, а не з відсотком.
Джерела
- ISTQB CTFL Syllabus v4.0 — розділ 4.3 «White-Box Test Techniques»: statement testing, branch testing і цінність білоскринькового підходу; ця глава покриває його і розширює до MC/DC та шляхів.
- ISTQB Glossary — канонічні означення statement coverage, branch coverage, MC/DC, cyclomatic complexity.
- Istanbul — документація класичного інструмента покриття для JS/TS: метрики, формати звітів, пороги.
- Playwright: Coverage API — збір покриття JS/CSS з браузера в e2e-тестах (Chromium).
Що таке білоскринькові техніки тест-дизайну і чим вони відрізняються від специфікаційних?
Білоскринькові, або структурні, техніки (white-box, structure-based) беруть за тест-базис сам код і його внутрішню структуру, тоді як специфікаційні (black-box) працюють від вимог, не заглядаючи всередину. Питання «що перевірити?» перетворюється на «які частини коду мої тести реально виконали, а які — ні». Практичних застосувань два: дизайн тестів — дивимось на код і додаємо тести, поки не виконається кожен оператор чи гілка, — і вимірювання, коли наявні тести (спроєктовані будь-якою технікою) запускають під інструментом покриття й дивляться, що вони зачепили. У командах coverage найчастіше живе саме у другій ролі — як метрика, а не як техніка дизайну.
Що таке statement coverage і як його рахують?
Покриття операторів (statement coverage) — частка виконуваних операторів коду, які тести виконали хоча б раз: виконані оператори поділити на всі виконувані та помножити на 100%. Це найслабша і найпростіша метрика: вона відповідає лише на питання «чи виконувався цей рядок логіки взагалі». Класична ілюстрація слабкості — функція з одним if без else: єдиний позитивний тест виконує всі оператори і дає 100%, хоча негативний сценарій не запускався жодного разу. Тому 100% statement — мінімальна база, а не привід зупинитися.
Чим statement coverage відрізняється від branch coverage? Чому 100% statement не гарантує 100% branch?
Statement рахує виконані оператори — вузли графа потоку керування (control flow graph), branch — відпрацьовані гілки рішень, тобто ребра цього графа: кожен if має спрацювати і в true, і в false. Розбіжність видно на if без else: один позитивний тест виконує всі оператори (вони всі лежать на гілці true), а порожня гілка false операторів не має — statement-метриці просто нема за що зачепитися, тож statement 100%, branch 50%. Практичний наслідок: непокрита гілка false — це той самий негативний сценарій, який ми шукаємо специфікаційними техніками, і улюблене місце багів на кшталт «забули обробити відсутність знижки/токена/зʼєднання». Сильна відповідь на співбесіді обовʼязково містить цей приклад, бо інтервʼюер перевіряє розуміння механізму, а не завчене означення.
Що таке правило підпорядкування (subsumption) метрик покриття?
Це правило каже: 100% сильнішої метрики автоматично гарантує 100% слабшої, а навпаки — ні. Так, повне покриття гілок дає повне покриття операторів, бо виконати всі ребра графа неможливо, не відвідавши всі вузли; зворотне неправильне — доказ той самий if без else. Ієрархія сили: statement, далі branch/decision, далі MC/DC, далі multiple condition; покриття шляхів стоїть окремо від цього ланцюжка. Кожен наступний рівень включає гарантії попереднього і коштує дорожче тестами. Саме тому силабус ISTQB CTFL 4.0 лишив пару statement + branch: перша метрика — мінімальна база, друга — розумний робочий стандарт.
Чим line coverage відрізняється від statement coverage?
Одиниці вимірювання різні: line рахує фізичні рядки файлу, statement — оператори, а в одному рядку операторів може бути кілька. У рядку if (a) b(); їх два, і рядкова метрика покаже 100%, коли виконалася лише перевірка умови, а виклик b() — ні. Через це line систематично оптимістичніша за statement, хоча в звітах вони часто йдуть поруч і виглядають синонімами. Практичний висновок: коли числа розходяться, довіряти варто statement, а ще краще — branch.
Що таке condition coverage і в чому його пастка?
Покриття умов (condition coverage) вимагає, щоб кожна атомарна умова всередині складеного рішення побувала і true, і false. Пастка в тому, що формальні 100% можливі без покриття гілок: для isVip && total > 1000 пара тестів (true, 500) і (false, 1500) дає кожній умові обидва значення, але саме рішення обидва рази false — гілка true жодного разу не виконана. Тобто покриття умов саме по собі не гарантує навіть branch coverage. Окремий нюанс реального коду: && у JS/TS обчислюється ліниво (short-circuit), тож при лівому false правий операнд узагалі не виконується; класична теорія цим нехтує, а інструменти на кшталт Istanbul рахують кожен операнд окремо.
Що таке MC/DC і навіщо воно, якщо вже є branch coverage?
MC/DC (modified condition/decision coverage) вимагає, щоб кожна атомарна умова не лише побувала в обох значеннях, а й незалежно вплинула на результат рішення: має існувати пара тестів, де змінюється лише ця умова — і разом з нею перевертається результат. Branch дивиться тільки на результат рішення загалом, тому окрема умова може жодного разу не «сіпнутися»: якщо її випадково інвертують, branch-тести цього не помітять. Для A && B MC/DC досягається трьома тестами: базовий true-випадок і два, де по черзі перемикається кожна умова. На співбесіді це улюблена перевірка, чи розрізняє кандидат рішення й атомарну умову.
Скільки тестів коштує MC/DC порівняно з повним перебором комбінацій умов?
Для рішення з n умов MC/DC досяжне щонайменше за n + 1 тестів, тоді як повне комбінаторне покриття (multiple condition coverage) коштує 2^n: для восьми умов це 9 тестів проти 256. Саме за цю економію зі збереженням сильної гарантії MC/DC вимагають стандарти критичного ПЗ: авіаційний DO-178C приписує його для софту найвищого рівня критичності (Level A). У вебі MC/DC руками майже не рахують, але розуміння принципу «n + 1 проти 2^n» — маркер глибини для middle+.
Чому 100% path coverage вважають теоретичним орієнтиром, а не робочою ціллю?
Покриття шляхів (path coverage) вимагає виконати кожен маршрут через функцію від входу до виходу, і кількість цих маршрутів росте експоненційно: три послідовні незалежні if дають 8 шляхів, десять — уже 1024. Цикл із заздалегідь невідомою кількістю ітерацій робить число шляхів практично нескінченним. Тому це найсильніша метрика — і практично недосяжна. Компромісна робоча відповідь — базисні шляхи через цикломатичну складність та branch coverage як реалістичний стандарт.
Що таке цикломатична складність і як нею користується QA?
Цикломатична складність (cyclomatic complexity) — метрика Томаса Маккейба 1976 року: кількість лінійно незалежних шляхів через код. Для графа потоку керування рахується як E − N + 2 (ребра мінус вузли плюс два), а на практиці простіше: кількість бінарних рішень плюс один — функція з одним if має складність 2, з if, циклом і && в умові — 4. Для QA це, по-перше, оцінка кількості тестів: складність — верхня межа тестів для повного branch coverage і нижня межа кількості всіх шляхів, тож функція зі складністю 12 означає щонайменше 12 базисних маршрутів. По-друге, сигнал ризику: висока складність корелює з дефектністю, тож функція-монстр зі складністю 20+ — аргумент і за рефакторинг, і за пріоритетне тестове покриття; лінтери на кшталт правила complexity в ESLint вміють її рахувати автоматично.
Які чотири числа показує типовий coverage-звіт і якому з них довіряти найбільше?
Istanbul/nyc, c8 та вбудовані репортери Jest і Vitest показують statements (частку виконаних операторів), branches (частку відпрацьованих гілок), functions (частку викликаних функцій) і lines (частку виконаних рядків). Найчесніше з чотирьох — branches: воно падає першим і саме за ним видно недотестовані сценарії, тоді як lines і statements оптимістичніші за побудовою. Робочий сценарій читання звіту — не дивитися на загальний відсоток, а спускатися у файли критичних модулів і дивитися, які саме гілки лишилися непокритими: у HTML-звіті Istanbul вони позначені маркерами I та E. Найчастіше сірими виявляються обробники помилок, ранні return на невалідних даних і catch-блоки — рівно ті негативні сценарії, які найдорожче пропустити.
Чи можна виміряти покриття коду з e2e-тестів?
Так: у Playwright є API page.coverage.startJSCoverage() / stopJSCoverage(), який збирає дані про виконаний у браузері код — працює лише в Chromium-браузерах. Це показує, які частини фронтенду e2e-сюїта взагалі не торкається, тобто дає карту дірок на рівні реальних користувацьких сценаріїв, а не юніт-тестів. У CI на покриття зазвичай вішають пороги — quality gate, який валить білд при падінні нижче заданого відсотка й не дає новому коду вливатися зовсім без тестів.
100% покриття коду означає, що багів немає?
Ні, і чекають упевненого «ні» з двома причинами. Перша: покриття вимірює виконання, а не перевірку — тест, який викликає функцію без жодного assert, дає рівно те саме покриття, що й тест із суворими перевірками, тож метрика зелена, а гарантій нуль. Друга: покриття сліпе до відсутнього коду — нереалізована вимога дає нуль рядків, і на відсоток вона не впливає; такі пропуски ловить трасування вимог, а не coverage. Бонус сильної відповіді: низьке покриття інформативніше за високе — 60% branch у модулі оплати це твердий факт, що 40% гілок не виконує жоден тест, і привід діяти, тоді як 100% ще нічого не гарантує.
Чим покриття коду відрізняється від покриття вимог?
Це різні осі вимірювання: покриття коду відповідає на питання «що з написаного виконано тестами», покриття вимог — «що з обіцяного перевірено». Можна мати 100% по коду і нуль по вимозі, яку забули реалізувати, — коду немає, вимірювати нічого. Тому coverage-звіт і матриця трасування вимог не замінюють одне одного, а доповнюють: перший шукає недотестований код, друга — недотестовані й нереалізовані обіцянки. У зрілій відповіді на співбесіді ці дві осі згадують разом.
Що таке ефект Гудхарта стосовно coverage і як виглядає здорова практика?
Ефект Гудхарта: щойно метрика стає ціллю (KPI), вона перестає бути хорошою метрикою, бо команда починає оптимізувати відсоток, а не якість. Для coverage це виглядає впізнавано: зʼявляються тести на геттери, тести без асертів, виключення «незручних» файлів зі збору покриття. Здорова практика — тримати поріг у CI як запобіжник від деградації (новий код не вливається зовсім без тестів), а рішення про глибину перевірок приймати за ризиком, не за цифрою. Тобто поріг — це страховка, а не ціль, до якої треба «дотягнути» будь-якими тестами.
У проєкті coverage 85%. Що ви зробите з цим числом?
Слабка відповідь — «підніму до 100%»: вона показує роботу з відсотком, а не зі звітом. Сильна послідовність інша: спершу зʼясувати, яка це метрика (85% lines і 85% branches — дуже різні факти), потім подивитися branch-покриття саме критичних модулів, а не середнє по проєкту. Далі — спуститися в звіт і знайти, які саме гілки непокриті: зазвичай це обробники помилок, ранні return і catch-блоки, тобто найдорожчі для пропуску негативні сценарії. Насамкінець звірити картину з покриттям вимог і ризиками: покриття коду не бачить нереалізованих вимог, тож саме по собі рішення не дає. Інтервʼюер цим питанням шукає людину, яка працює зі звітом як з картою дірок, а не з KPI.
Три кейси: рахуємо всі рівні покриття руками на одній функції з складеним рішенням, читаємо реальний Istanbul-звіт як карту дірок (а не як відсоток) і розбираємо робочу ситуацію «coverage впав, мердж заблоковано». Скрізь — що дивитися і чому.
Кейс 1. Одна функція — чотири рівні покриття руками
Функція доставки: безкоштовно лише VIP-клієнтам із чеком понад 500.
function shippingCost(isVip: boolean, total: number): number {
let cost = 50; // оператор 1
if (isVip && total > 500) { // оператор 2: рішення з двох умов
cost = 0; // оператор 3
}
return cost; // оператор 4
}
Порівняємо три набори тестів (пари — це (isVip, total)):
| Набір тестів | Statement | Branch | Condition | MC/DC |
|---|---|---|---|---|
(true, 600) | 100% | 50% | ні | ні |
(true, 100) + (false, 600) | 75% | 50% | 100% | ні |
(true, 600) + (false, 600) + (true, 100) | 100% | 100% | 100% | 100% |
Що дивитися і чому:
- Рядок 1 — класика
ifбезelse. Один позитивний тест виконує всі чотири оператори (100% statement), але гілка false лишається невиконаною — branch 50%. Якби хтось зламав код так, що знижка на доставку дістається всім, цей «повністю покритий» тест лишився б зеленим. - Рядок 2 — пастка condition coverage. Кожна атомарна умова побувала і true, і false — формально 100% умов. Але рішення обидва рази false: оператор 3 не виконано взагалі (statement впав до 75%), гілка true не відпрацювала. Покриття умов саме по собі не гарантує навіть покриття гілок — улюблене підступне питання співбесід.
- Рядок 3 — MC/DC за n + 1 тестів. Порівняно з базовим
(true, 600)у другому тесті змінюється лишеisVip— результат перевертається; у третьому лише друга умова — результат знову перевертається. Кожна умова довела незалежний вплив трьома тестами замість чотирьох комбінаторних. - Нюанс short-circuit. У тесті
(false, 600)умоваtotal > 500через ліниве обчислення&&не виконується взагалі — Istanbul покаже операнд як непокритий, хоча «на папері» умова значення має. Класична теорія і реальний інструмент тут рахують по-різному.
Кейс 2. Istanbul-звіт: відсоток — не відповідь, відповідь — які саме гілки сірі
Текстовий підсумок після прогону юніт-тестів модуля оплати:
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
payment.ts | 92.3 | 57.1 | 100 | 94.0 | 45-52, 78
validators.ts | 100 | 100 | 100 | 100 |
retry.ts | 88.9 | 50.0 | 66.7 | 90.0 | 12, 30-41
Що дивитися і чому:
- Розрив між Stmts і Branch — головний сигнал. У
payment.tsоператори майже всі виконані (92.3%), а гілки — трохи більше половини. Так виглядає код, де багатоifбезelseі ранніхreturn: позитивні сценарії пройдені, негативні — ні. Саме тому branch — найчесніше з чотирьох чисел: воно падає першим. - Uncovered Line #s — це адреси, а не статистика. Рядки 45–52 у
payment.ts— майже напевноcatch-блок або обробка відмови платіжного шлюзу: рівно той сценарій, який найдорожче пропустити в проді. У HTML-звіті ці місця підсвічені червоним, а біля рішень стоять маркериIтаE— гілка if або else, яку жоден тест не виконав. - 100% у
validators.tsще нічого не гарантує. Тест нижче дає ті самі 100%, не перевіряючи нічого:
test('coverage є, перевірки немає', () => {
validateCard('4111111111111111');
validateCard('сміття');
// жодного expect — а statement і branch зелені
});
- Functions 66.7% у
retry.tsозначає, що якась функція не викликалася жодного разу — часто це експортований хелпер, який або мертвий код, або дірка в тестах. Відсоток сам не скаже, що з двох: треба відкрити файл.
Кейс 3. CI: «coverage впав з 84% до 81%, мердж заблоковано». Що робити?
Робоча ситуація: ваш PR з автотестами й новим хелпером заблоковано quality gate. Слабка реакція — накидати тестів на геттери, поки цифра не повернеться. Розбір по кроках:
- Зʼясувати, яка метрика впала. 84% чого — lines чи branches? Гейти часто рахують кілька порогів окремо. Падіння lines на 3% і падіння branches на 3% — різні за вагою факти: друге означає невиконані сценарії, перше може бути кількома довгими необхідними рядками.
- Подивитися покриття diff-а, а не проєкту. Загальний відсоток впав, бо в PR додано новий код без тестів — звіт покаже, який саме. Питання не «як повернути 84%», а «які гілки нового коду не виконуються жодним тестом».
- Оцінити непокрите за ризиком. Якщо сірими лишилися
catchі валідація вводу нового хелпера — це треба покрити по-справжньому, з асертами на поведінку. Якщо сірим лишився тривіальний геттер — чесніше сказати про це в ревʼю, ніж писати тест заради відсотка. - Не оптимізувати цифру. Тест без асертів чи виключення файлу зі збору піднімуть відсоток і обнулять сенс гейта — це ефект Гудхарта в дії. Поріг існує як запобіжник від деградації, а не як ціль.
Окремий інструмент для повноти картини: покриття можна зняти і з e2e-прогону — page.coverage.startJSCoverage() / stopJSCoverage() у Playwright (лише Chromium) покаже, які частини фронтенду ваша сюїта взагалі не виконує. Це корисно саме як карта дірок рівня користувацьких сценаріїв, коли юніт-покриття виглядає пристойно, а впевненості в критичних флоу немає.
Основи білоскринькового підходу
- Розумію різницю тест-базисів: специфікаційні техніки йдуть від вимог, білоскринькові — від структури коду.
- Знаю два застосування покриття: дизайн тестів від коду і вимірювання повноти наявних тестів; у командах домінує друге.
- Можу пояснити принцип «що дрібніша одиниця покриття (оператор, гілка, умова, шлях) — то сильніша гарантія і дорожчий набір тестів».
Statement vs branch
- Знаю формулу statement coverage (виконані оператори / усі виконувані × 100%) і чому це найслабша метрика.
- Можу на прикладі
ifбезelseпоказати, чому один позитивний тест дає 100% statement і лише 50% branch: порожня гілка false не має операторів — і саме там живуть баги «забули обробити відсутність знижки/токена/зʼєднання». - Розумію, що branch рахує ребра графа потоку керування, а statement — вузли, і чим branch формально відрізняється від decision (на рівні 100% вони збігаються).
- Знаю правило підпорядкування (subsumption): 100% branch гарантує 100% statement, навпаки — ні; тому ISTQB CTFL 4.0 лишив саме цю пару.
Умови та MC/DC
- Розрізняю рішення (весь логічний вираз) і атомарну умову всередині нього.
- Можу показати пастку condition coverage: кожна умова побувала true і false, а рішення завжди false — 100% умов без покриття гілок.
- Можу пояснити MC/DC: кожна умова незалежно перевертає результат рішення; ціна — n + 1 тестів проти 2^n у multiple condition coverage; обовʼязкове для критичного ПЗ (DO-178C, Level A).
- Памʼятаю нюанс short-circuit: при лівому false правий операнд
&&не виконується взагалі, а Istanbul рахує кожен операнд окремо.
Шляхи і цикломатична складність
- Розумію, чому 100% path coverage — теоретичний орієнтир: шляхи ростуть експоненційно, а цикли роблять їх число практично нескінченним.
- Знаю цикломатичну складність Маккейба: кількість лінійно незалежних шляхів; рахую як «бінарні рішення + 1» (формально
E − N + 2). - Можу застосувати складність практично: верхня межа тестів для повного branch coverage і сигнал ризику для пріоритизації тестування й рефакторингу.
Coverage-звіт і пастки метрики
- Знаю чотири числа звіту (statements, branches, functions, lines) і чому branch — найчесніше: падає першим і показує недотестовані сценарії.
- Розумію різницю lines vs statements: у рядку
if (a) b();два оператори, тож рядкова метрика систематично оптимістичніша. - Вмію читати HTML-звіт Istanbul: червоні непокриті оператори, маркери
I/Eна гілках; типові сірі місця — обробники помилок, ранніreturn,catch-блоки. - Знаю дві сліпі плями метрики: вона вимірює виконання, а не перевірку (тест без assert дає ті самі відсотки), і не бачить ненаписаного коду (нереалізовану вимогу ловить трасування вимог).
- Можу пояснити ефект Гудхарта для coverage-KPI (тести на геттери, без асертів, виключення файлів) і здорову роль порога — запобіжник від деградації, не ціль.
- Знаю, що покриття можна зняти і з e2e: Playwright
page.coverage.startJSCoverage()(лише Chromium) показує, який фронтенд-код сюїта не торкається.
Що служить тест-базисом для білоскринькових технік?
Питання
Чим білоскринькові (white-box) техніки відрізняються від специфікаційних за тест-базисом?