White-box-техніки: покриття коду
Зміст
У pull request автотестів зʼявляється коментар від CI: «coverage впав з 84% до 81%, мердж заблоковано». Що саме впало? Чи означають ці 84%, що 84% поведінки продукту перевірено? І чому розробник поруч каже, що їхні 100% на модулі оплати «нічого не гарантують»? Без розуміння метрик коду ці розмови звучать як шаманство — а питання «чим statement coverage відрізняється від branch coverage» за дві хвилини показує, чи розуміє людина, що вимірює метрика, якою всі звітують.
Це глава-поглиблення: при першому проході розділу її можна пропустити — базовий трек від класів еквівалентності до вибору технік самодостатній без неї. Повертайтеся сюди, коли почнете писати автотести або коли у вашій команді coverage стане критерієм якості . Водночас це єдина глава на сайті, де тема покриття коду розібрана повністю, — решта глав лише посилаються сюди.
Що таке white-box-техніки
Усі техніки попередніх глав — специфікаційні (): служать вимоги, а код лишається black-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; кожна умова циклу — і «зайшли», і «пропустили». Одиниця вимірювання — не інструкція, а ребро на графі потоку керування (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) із white-box-технік лишили саме цю пару — statement і branch: перша як мінімальна база, друга як розумний робочий стандарт.
На практиці branch досі ототожнюють з покриттям рішень (decision coverage), і на співбесіді ви почуєте обидві назви — але для ISTQB це не синоніми. Версія 4.0 замінила decision на branch і пояснює заміну в release notes (Appendix C): «different standards define the decision differently, as opposed to “branch”», а стару тезу FL2018 «100% decision coverage implies 100% statement coverage» названо там дослівно — «this sentence is not true in case of programs with no decisions». Різниця вимірна: гілка за §4.3.2 — це передача керування між двома вузлами графа, і вона буває як умовною (результат рішення), так і безумовною (прямолінійний код), а рішень у програмі може не бути жодного. Саме тому subsumption вище тримається для branch і ламалося для decision. У обидва терміни живі, але з різними означеннями: branch coverage — «The coverage of branches in a control flow graph», decision coverage — «The coverage of decision outcomes». Тобто decision coverage — історична назва в цій главі, а не другий спосіб сказати branch coverage.
Практична вага для тестувальника: гілка 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.
Підсумок
- White-box-техніки беруть за тестову основу код: одиниця покриття — інструкція, гілка, умова або шлях; що дрібніша одиниця, то сильніша гарантія і дорожчий набір тестів.
- Ієрархія сили: 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® Certified Tester Foundation Level Syllabus v4.0.1 — означення родини (аналіз внутрішньої структури ), її обмеження — кейси зʼявляються лише після дизайну чи реалізації — і аргумент за вимірювання:
black-boxміри реального покриття коду не дає.
Покриття інструкцій (statement coverage)
- ISTQB® Certified Tester Foundation Level Syllabus v4.0.1 — — виконувана інструкція (не рядок файлу), формула частки й межа 100%: дефекти, залежні від даних, і невиконані гілки лишаються поза нею.
Покриття гілок (branch coverage) і чому 100% statement ≠ 100% branch
- ISTQB® Certified Tester Foundation Level Syllabus v4.0.1 — гілка як передача керування між двома вузлами графа, умовна або безумовна; правило
subsumes(100% гілок дає 100% інструкцій, не навпаки) і три причини, з яких v4.0 замінилаdecisionнаbranch— включно з хибністю старої тези для програм без рішень.
- ISTQB Glossary — машинний зріз усіх термінів — означення всіх трьох рівнів цієї таблиці:
condition coverage,modified condition/decision coverage(через атомарні умови, що незалежно впливають на рішення) іmultiple condition coverageяк покриття всіх комбінацій. - ISTQB® Certified Tester Foundation Level Syllabus v4.0.1 — межа викладу: суворіші структурні техніки для safety- й mission-critical систем силабус називає, але свідомо не розглядає.
Шляхи і цикломатична складність
- ISTQB Glossary — машинний зріз усіх термінів — канонічне означення метрики:
cyclomatic complexity— «The maximum number of linear, independent paths through a program», тобто саме «кількість лінійно незалежних шляхів через код». Покриття шляхів (path coverage) канон не означує взагалі — 0 входжень і в силабусі CTFL, і в усіх 651 записі глосарія.
- Istanbul — JavaScript test coverage made simple — механізм самого виміру: інструмент вставляє в код лічильники, а
nyc— його CLI, який стикується з більшістю ранерів і вміє збирати покриття з підпроцесів. - nyc — README проєкту Istanbul — чотири незалежні пороги (
branches,lines,functions,statements), окремийcheck-coverage, що за замовчуванням вимкнений, і семантика кольорів звіту: watermark — не поріг білда. - Playwright — class Coverage — збір покриття JS і CSS зі сторінки: лише Chromium, скидання на кожній навігації за замовчуванням і конвертація виходу V8 у формат Istanbul.
- Brian Marick — How to Misuse Code Coverage — першоджерело пастки: гейт на відсотку ламає метрику, бо «people optimize their performance according to how they're measured», а принципова сліпа зона інструмента —
faults of omission: він працює лише з тим кодом, який йому дали. - Martin Fowler — TestCoverage — та сама теза пізнішим коментарем: зробиш рівень покриття ціллю — його досягнуть, бо високі числа легко набити низькоякісними тестами; 100% автор читає як підозрілий сигнал.
Що таке white-box-техніки тест-дизайну і чим вони відрізняються від специфікаційних?
-техніки, або структурні (structure-based), беруть за сам код і його внутрішню структуру, тоді як специфікаційні () працюють від вимог, не заглядаючи всередину. Питання «що перевірити?» перетворюється на «які частини коду мої тести реально виконали, а які — ні». Практичних застосувань два: дизайн тестів — дивимось на код і додаємо тести, поки не виконається кожна інструкція чи гілка, — і вимірювання, коли наявні тести (спроєктовані будь-якою технікою) запускають під інструментом й дивляться, що вони зачепили. У командах 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, далі 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) покаже, які частини фронтенду ваша взагалі не виконує. Це корисно саме як карта дірок рівня користувацьких сценаріїв, коли unit-покриття виглядає пристойно, а впевненості в критичних флоу немає.
Основи white-box-підходу
- Розумію різницю : специфікаційні техніки йдуть від вимог, — від структури коду.
- Знаю два застосування : дизайн тестів від коду і вимірювання повноти наявних тестів; у командах домінує друге.
- Можу пояснити принцип «що дрібніша одиниця покриття (інструкція, гілка, умова, шлях) — то сильніша гарантія і дорожчий набір тестів».
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) показує, який фронтенд-код не торкається.
Квіз
Перед стартом
- Питань: 15
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Чим white-box-техніки відрізняються від специфікаційних за тестовою основою?

