vyvchy
    Теми розділу

    02 · Тест-дизайн

    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 як на граф:

    true

    false

    total = price

    isVip?

    total = total * 0.9

    return total

    true

    false

    total = price

    isVip?

    total = total * 0.9

    return total

    Тест 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, і false2
    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:

    1. (true, 1500) → true
    2. (false, 1500) → false — порівняно з першим змінився лише isVip, результат перевернувся: isVip впливає;
    3. (true, 500) → false — порівняно з першим змінилася лише друга умова, результат перевернувся: вона теж впливає.

    Три тести замість чотирьох комбінаторних. Загальне правило: для рішення з n умов MC/DC досяжне щонайменше за n + 1 тестів, тоді як повний перебір коштує 2^n — для восьми умов це 9 тестів проти 256. Саме за цю економію зі збереженням сильної гарантії MC/DC вимагають стандарти критичного ПЗ: авіаційний DO-178C приписує MC/DC для софту найвищого рівня критичності (Level A). У вебі MC/DC руками майже не рахують, але саме на ньому видно різницю між рішенням і умовою.

    Ієрархія сили метрик:

    Statement

    Branch / Decision

    MC/DC

    Multiple condition

    Statement

    Branch / Decision

    MC/DC

    Multiple condition

    Кожна наступна включає гарантії попередньої і коштує дорожче. Окремо від цього ланцюжка стоїть покриття шляхів — про нього далі.

    Шляхи і цикломатична складність

    Покриття шляхів (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-покриття критичних модулів, знайду непокриті гілки обробки помилок, звірю з покриттям вимог і ризиками». Інтервʼюер шукає людину, яка працює зі звітом, а не з відсотком.

    Джерела

    Що таке white-box-техніки

    • 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 — включно з хибністю старої тези для програм без рішень.

    Покриття умов і MC/DC

    • 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 записі глосарія.

    Як читати coverage-звіт

    • 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.

    Пастка «100% покриття»

    • Brian Marick — How to Misuse Code Coverage — першоджерело пастки: гейт на відсотку ламає метрику, бо «people optimize their performance according to how they're measured», а принципова сліпа зона інструмента — faults of omission: він працює лише з тим кодом, який йому дали.
    • Martin Fowler — TestCoverage — та сама теза пізнішим коментарем: зробиш рівень покриття ціллю — його досягнуть, бо високі числа легко набити низькоякісними тестами; 100% автор читає як підозрілий сигнал.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.