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

    03 · Веб і мережі для AQA

    Практичні сценарії AQA: флак і синхронізація

    Зміст

    Флак (flakiness) — це коли той самий тест на тому самому коді іноді зелений, а іноді червоний, хоча ти нічого не змінював. Це не «магія» й не «поганий день у CI». За кожним флаком стоїть конкретна причина: тест і застосунок живуть у різному часі. Браузер працює асинхронно — довантажує розмітку, виконує JavaScript, ходить у мережу, малює анімації — а тест припускає, що все вже сталося. Розрив між «сторінка ще думає» і «тест уже клікає» і є джерелом більшості нестабільних падінь.

    Синхронізація (synchronization) — це вміння поставити тест і застосунок «в один такт»: діяти рівно тоді, коли застосунок готовий, і не раніше. Нижче — типові сценарії, з якими стикається кожен AQA, і механіка того, чому вони ламаються.

    Клік по кнопці, якої ще немає

    Найпоширеніший випадок. Тест відкриває сторінку й одразу шукає кнопку «Зберегти», а вона зʼявляється лише після того, як відпрацює асинхронний запит і фреймворк домалює розмітку.

    Наївний код:

    // Погано: елемент може ще не існувати
    const btn = document.querySelector('#save');
    btn.click(); // TypeError: Cannot read properties of null

    У Selenium без очікування це дасть NoSuchElementException, бо пошук елемента виконується миттєво в момент виклику. Правильна відповідь — очікування (wait) на появу елемента:

    // Playwright: локатор лінивий, дія сама дочекається появи
    await page.locator('#save').click();

    Тут працює авто-очікування (auto-wait): Playwright не шукає елемент одразу, а повторює спроби до таймауту, поки той не зʼявиться в DOM і не стане придатним до дії. У Selenium той самий ефект дає явне очікування (explicit wait):

    // Selenium (WebDriverWait): чекаємо появу до 10 секунд
    const btn = await driver.wait(
      until.elementLocated(By.css('#save')),
      10000
    );
    await btn.click();

    Ключова думка: локатор (locator) — це не сам елемент, а рецепт, як його знайти. Знаходити треба в момент дії, а не наперед, і давати застосунку час доробити своє.

    Елемент є в DOM, але обробник ще не навісився

    Підступніший сценарій. Кнопка вже в розмітці, видима, активна — а клік «нічого не робить». Причина: JavaScript ще не привʼязав обробник події (event handler) до елемента. Так буває при гідратації (hydration) у React/Vue/Svelte, коли сервер віддає готовий HTML, а інтерактивність «оживає» лише після завантаження й виконання клієнтського бандлу.

    Це варто усвідомити чесно: авто-очікування тут не рятує. Playwright перевіряє, що елемент прикріплений, видимий, стабільний і приймає події, — але він фізично не може дізнатися, чи навішений onClick у коді застосунку. Для браузера кнопка вже «клікабельна».

    Правильний підхід — чекати на прикладний сигнал готовності (application-level signal), а не на сам факт наявності елемента:

    // Варіант 1: чекаємо зникнення індикатора завантаження
    await page.getByRole('button', { name: 'Save' }).waitFor();
    await page.locator('.spinner').waitFor({ state: 'hidden' });
    await page.getByRole('button', { name: 'Save' }).click();
    
    // Варіант 2: застосунок сам позначає готовність атрибутом
    await page.locator('[data-hydrated="true"]').waitFor();

    Нюанс. Атрибут-маркер готовності (data-hydrated, aria-busy="false" тощо) — річ конкретного застосунку, а не стандарт. Узгодь із розробниками, який сигнал вважати «інтерактивно готово», і чекай саме його.

    Мораль: домовляйтеся з командою про явний маркер готовності. Тест, який мовчки клікає в порожнечу, гірший за той, що чесно падає.

    AJAX-сабміт і правильне очікування результату

    Форма з асинхронною відправкою (AJAX submit) — класична пастка. Тест заповнює поля, тисне «Надіслати» й одразу перевіряє повідомлення про успіх. Але запит ще в дорозі.

    Хибний ланцюжок:

    await page.getByLabel('Email').fill('a@b.com');
    await page.getByRole('button', { name: 'Submit' }).click();
    // Погано: перевіряємо до того, як прийшла відповідь
    await expect(page.getByText('Готово')).toBeVisible(); // флак

    Тут частково рятує авто-очікування toBeVisible: web-first-твердження саме перечікує до таймауту, повторно опитуючи умову. Але надійніше синхронізуватися безпосередньо з тією подією, на яку ти чекаєш. Є два рівні.

    // Рівень 1: чекаємо саме мережеву відповідь
    const respPromise = page.waitForResponse(
      r => r.url().includes('/api/subscribe') && r.status() === 200
    );
    await page.getByRole('button', { name: 'Submit' }).click();
    await respPromise;
    await expect(page.getByText('Готово')).toBeVisible();

    Зверни увагу: проміс відповіді створюють до кліку. Інакше між кліком і викликом waitForResponse є вікно, в яке швидка відповідь може прослизнути непоміченою, — це знову гонка.

    СерверБраузерТестСерверБраузерТестЯкби проміс створили ПІСЛЯ кліку,швидка відповідь прослизнула б повз (гонка)Створити waitForResponse (до кліку)Клік «Submit»POST /api/subscribe200 OKПроміс відповіді виконаноexpect(«Готово») — перевірка результатуСерверБраузерТестСерверБраузерТестЯкби проміс створили ПІСЛЯ кліку,швидка відповідь прослизнула б повз (гонка)Створити waitForResponse (до кліку)Клік «Submit»POST /api/subscribe200 OKПроміс відповіді виконаноexpect(«Готово») — перевірка результату
    // Рівень 2: чекаємо видимий наслідок у UI (найближче до користувача)
    await page.getByRole('button', { name: 'Submit' }).click();
    await expect(page.getByRole('alert')).toHaveText('Готово');

    Що обрати? Правило таке: перевіряй те, що бачить користувач (UI-наслідок), а на мережеву відповідь синхронізуйся тоді, коли зміна в UI непомітна або надто «тонка», щоб на неї надійно зачепитися. Іноді доречно поєднати: дочекатися відповіді, щоб зняти гонку, і окремо стверджувати результат в інтерфейсі.

    Клік не спрацьовує: елемент перекрито модалкою або тултіпом

    Елемент є, він видимий, обробник навішений — а клік летить «повз». Причина в тому, що поверх нього лежить інший елемент: модальне вікно (modal), напівпрозорий оверлей (overlay), тултіп (tooltip), «липкий» хедер чи cookie-банер.

    Браузер відправляє клік у точку екрана, і подію ловить верхній за z-порядком елемент, а не той, у який ти цілився. Playwright робить перевірку «отримує події» (receives events) — так званий hit-test: перевіряє, що саме цільовий елемент є верхнім у точці кліку. Якщо ні — повторює спроби й падає з повідомленням у стилі:

    <div class="overlay">…</div> intercepts pointer events

    Це насправді корисне падіння: тест зловив реальну проблему UX або власну помилку. Що робити:

    Причина перекриттяПравильна реакція
    Модалка ще не закриласьДочекатися її зникнення: .waitFor({ state: 'hidden' })
    Cookie-банер / оверлейЗакрити його в передумові (precondition), а не «пробивати»
    Тултіп зʼявляється при наведенніПрибрати фокус / навести деінде або клікнути інший якір
    Анімація ще їдеДочекатися стабільності (див. розділ про анімації)

    Чого не робити — не бити «силовим» кліком наосліп:

    // Погано: обходимо перевірки й ховаємо реальний баг
    await page.locator('#save').click({ force: true });

    force: true вимикає неосновні перевірки актуальності (зокрема «отримує події») й імітує клік попри перекриття. Це доречно у виняткових випадках (наприклад, свідомо тестуєш поведінку під оверлеєм), але за замовчуванням це спосіб зробити тест зеленим, приховавши дефект. Спершу спитай себе: а живий користувач узагалі зміг би сюди клікнути?

    Анімації заважають кліку і скріншоту

    Анімації й переходи (transitions) псують дві речі. Перше — клік: елемент їде, і його координати змінюються між моментом наведення й моментом натискання. Playwright тут вимагає стабільності (stable) — щоб bounding box не змінювався щонайменше два послідовні кадри анімації; поки елемент їде, дія перечікується. Друге — скріншоти: візуальне порівняння (visual regression) ловить проміжний кадр анімації, і дифф червоніє щоразу по-різному.

    Найнадійніше — глушити анімації на час тесту:

    // Вимикаємо анімації через CSS-інʼєкцію
    await page.addStyleTag({ content: `
      *, *::before, *::after {
        animation-duration: 0s !important;
        transition-duration: 0s !important;
      }
    `});

    Playwright у скріншотах має вбудований варіант:

    // Стабілізує анімації на час знімка
    await expect(page).toHaveScreenshot({ animations: 'disabled' });

    Значення 'disabled' (воно ж дефолтне для toHaveScreenshot) зупиняє CSS-анімації, CSS-переходи й анімації Web Animations API: скінченні анімації промотуються до кінцевого стану, нескінченні — скидаються до початкового й програються далі після знімка. Але воно не чіпає анімації, намальовані вручну через requestAnimationFrame чи на <canvas>, — їх, якщо вони заважають, доводиться глушити на рівні самого застосунку.

    Проміжна сторінка під час редіректу

    Логін часто працює так: сабміт форми → сервер відповідає редіректом → браузер вантажить проміжну сторінку («Зачекайте…») → фінальний редірект на дашборд. Тест, який одразу після кліку перевіряє URL дашборда, ловить проміжний стан і падає.

    СерверБраузерТестСерверБраузерТестwaitForURL('**/dashboard') — чекаємолише кінцевий стан, ігноруючи проміжніКлік «Login»POST /login302 → /waitGET /wait200 «Зачекайте…» (проміжна сторінка)GET /dashboard200 ДашбордСерверБраузерТестСерверБраузерТестwaitForURL('**/dashboard') — чекаємолише кінцевий стан, ігноруючи проміжніКлік «Login»POST /login302 → /waitGET /wait200 «Зачекайте…» (проміжна сторінка)GET /dashboard200 Дашборд

    Коротко про коди редіректів, бо вони визначають поведінку браузера:

    КодНазваМетод при переході
    301Moved PermanentlyІсторично часто змінюють на GET
    302FoundІсторично часто змінюють на GET
    303See OtherЗавжди GET
    307Temporary RedirectЗберігає метод (POST лишається POST)
    308Permanent RedirectЗберігає метод

    Практичний висновок для тесту: не привʼязуйся до проміжних URL і не роби припущень про кількість «стрибків». Чекай саме на фінальний стан:

    // Чекаємо кінцевий URL за патерном, ігноруючи проміжні
    await page.getByRole('button', { name: 'Login' }).click();
    await page.waitForURL('**/dashboard');
    await expect(page.getByRole('heading', { name: 'Огляд' })).toBeVisible();

    Ще одна тонкість — стан завантаження (load state). Подія domcontentloaded настає, коли розібрано HTML, але підресурси й дані ще можуть вантажитися; load — коли завантажено й підресурси. Не бий у сторінку між редіректами: якщо синхронізуватися на domcontentloaded проміжної сторінки, вона за мить зникне, і твій локатор відвалиться разом із нею. Надійніше — очікувати саме кінцевий видимий елемент дашборда, а не абстрактне «сторінка завантажилась».

    Залежність від нестабільного зовнішнього API

    Тест, який реально ходить у сторонній сервіс (платіжний шлюз, погода, карти, OAuth-провайдер), успадковує всю його нестабільність: таймаути, ліміти запитів (rate limits), зміни у відповідях, падіння в неробочі години. Такий тест червоніє не тому, що зламався твій застосунок, а тому, що «застудився» чужий.

    Розділяй наміри. Якщо ти перевіряєш свою логіку, зовнішній сервіс треба ізолювати через мок (mock) або стаб (stub) на рівні мережі:

    // Перехоплюємо запит і віддаємо детерміновану відповідь
    await page.route('**/api.exchange.com/rates', route =>
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ USD: 41.2, EUR: 44.8 })
      })
    );

    Це прибирає флак і дозволяє легко перевірити крайні випадки: помилку 500, порожню відповідь, повільну відповідь. Аналог у Cypress — cy.intercept.

    Окремо варто мати контрактні або end-to-end тести, які таки бʼються об реальний сервіс, — але їх виносять в окрему повільну категорію, ганяють рідше й не блокують ними основний пайплайн. Змішувати одне з іншим — прямий шлях до «червоного, якому ніхто не вірить».

    Чому hard sleep — погано

    Жорстка пауза (hard sleep) — це sleep(3) замість очікування конкретної умови. Її ставлять «щоб напевно». Насправді вона програє двічі:

    • Занадто мало — на повільному CI трьох секунд не вистачить, тест флакне.
    • Занадто багато — на швидкій машині ти щоразу марно чекаєш повний інтервал, і сотні таких пауз перетворюють десятихвилинний прогін на годинний.

    Головна проблема: sleep синхронізується з годинником, а не з застосунком. Він не знає, готова система чи ні. Розумне очікування опитує реальну умову й завершується в ту саму мілісекунду, коли вона стала істинною.

    КритерійHard sleepРозумне очікування
    На що чекаєФіксований часКонкретну умову
    На швидкій машиніМарно гає часЗавершується одразу
    На повільному CIМоже не дочекатисьДочекається (в межах таймауту)
    Ловить проблему раноНіТак, падає з причиною

    Єдиний умовно-виправданий sleep — коли треба спостерегти відсутність зміни (наприклад, «після дії нічого не має статися»), бо чекати на «нічого» нема як. Але й тут краще підтвердити стан через прикладний сигнал, а не голий таймер.

    Explicit wait проти auto-wait

    Два підходи до синхронізації, які часто плутають.

    • Явне очікування (explicit wait) — ти сам пишеш умову й таймаут: «чекай, поки цей елемент стане видимим, до 10 секунд». Класика Selenium (WebDriverWait + expected_conditions).
    • Авто-очікування (auto-wait) — інструмент перед кожною дією сам перевіряє набір умов актуальності (actionability) і перечікує до таймауту. Так працює Playwright і, частково, Cypress.

    Що перевіряє авто-очікування Playwright перед кліком:

    ПеревіркаЩо означає
    AttachedЕлемент є в DOM
    VisibleМає непорожній bounding box і не visibility:hidden (елемент з opacity:0 вважається видимим)
    StableНе змінює bounding box два послідовні кадри анімації
    Receives eventsЄ верхнім елементом у точці дії (hit-test)
    EnabledНе має атрибута disabled

    Для дій уведення (наприклад, fill) додається ще перевірка Editable — елемент не має бути readonly.

    Порівняння підходів:

    Explicit waitAuto-wait
    Хто пише умовуІнженер вручнуІнструмент за замовчуванням
    Ризик забути очікуванняВисокийНизький
    Гнучкість під нетипову умовуВисокаТреба доповнювати вручну
    Типовий інструментSeleniumPlaywright

    Є ще третій, історичний механізм — неявне очікування (implicit wait) у Selenium: глобальний таймаут, протягом якого драйвер повторює пошук елемента. Він вузький (реагує лише на «елемент не знайдено», не вміє чекати на видимість чи клікабельність), а головне — змішування неявного й явного очікування дає непередбачувані сумарні таймаути. Тому загальна рекомендація — обрати щось одне; на практиці це явне очікування.

    Важлива чесність: авто-очікування не всесильне. Воно закриває «елемент зʼявився / став клікабельним», але не знає про прикладні стани — навішений обробник, завершений AJAX, оновлені дані. Там усе одно потрібне явне очікування на прикладний сигнал (див. розділи вище). Найкращі набори тестів комбінують: покладаються на авто-очікування для рутини й додають явні очікування там, де готовність визначає сам застосунок. І в жодному разі не використовуй waitForTimeout/sleep як заміну умові.

    Тест зелений локально, але флакне на CI

    Це найчастіше питання на співбесідах і найбільший біль на практиці. Локальна машина й CI — різні світи. Найтиповіші причини такі.

    1. Ресурси й швидкість. CI-раннер зазвичай слабший і завантаженіший: менше CPU, повільніший диск, сусідні джоби борються за ядра. Те, що локально стається за 200 мс, на CI займає 2 с — і жорсткі таймаути/паузи не витримують. Лікування: розумні очікування замість sleep, адекватні (не занадто короткі) таймаути, менше воркерів на слабкому раннері.

    2. Headless проти headed і розмір вікна. Локально ти дивишся тест у видимому браузері певної роздільності; CI гонить headless із дефолтним, часто меншим, viewport. Елемент, який локально в зоні видимості, на CI опиняється за межами видимої області (below the fold) або зʼїжджає в мобільний лейаут — і клік не проходить. Лікування: фіксуй viewport у конфізі й ганяй локально в тому самому режимі, що й CI.

    3. Часовий пояс, локаль і дані середовища. CI нерідко в UTC та англійській локалі, а локальна машина — у Києві й українською. Тест, що звіряє відформатовану дату, суму чи текст, ламається на розбіжності TZ/LANG. Плюс порожня чи інакше засіяна база на CI. Лікування: фіксуй TZ, локаль і Accept-Language, не покладайся на «сьогоднішню» дату без мокання часу.

    Інші часті винуватці: гонки при паралельному прогоні (див. нижче), відсутні системні шрифти (ламають візуальні тести), різні версії браузера, мережеві обмеження CI-мережі. Практичний прийом — відтворити умови CI локально: той самий Docker-образ, headless, той самий viewport, TZ=UTC, обмежені CPU. Флак, який «не ловиться локально», майже завжди ловиться, щойно локальне середовище зробити схожим на CI.

    Протікання даних між тестами й ізоляція

    Протікання стану (state leakage) — це коли один тест лишає по собі слід, а інший на цей слід спотикається. Класика: тест A створює користувача test@x.com, тест B теж намагається його створити й падає на «вже існує». Або тести залежать від порядку виконання — і варто змінити порядок чи ввімкнути паралель, як усе сиплеться.

    Спільний стан протікає через: базу даних, localStorage/cookies/сесію, кеш, зовнішні черги, глобальні змінні в самому тест-фреймворку.

    Принципи ізоляції (isolation):

    • Свіжий контекст на тест. У Playwright кожен тест за замовчуванням отримує чистий browser context — окремі cookies й storage, нема витоку сесії між тестами.
    • Унікальні дані. Не хардкодь test@x.com. Генеруй унікальні значення на кожен запуск:
    // Унікальний email рятує від колізій між тестами й прогонами
    const email = `user_${Date.now()}_${crypto.randomUUID()}@example.com`;
    • Кожен тест сам створює свої передумови й прибирає за собою — не спирайся на дані, які «мав лишити попередній тест».
    • Прибирання (teardown) роби в afterEach/фікстурі, а не наприкінці «щасливого» шляху, бо при падінні дія до прибирання не дійде.

    Нюанс. На спільному стенді (shared environment) очищення тестових даних може зачепити чужі — стратегію teardown там узгоджуй окремо; синтетичні дані завжди безпечніші за реальні.

    Чому падає паралельний прогін

    Паралель (parallel execution) вмикають заради швидкості — і одразу випливають дефекти ізоляції, яких не було видно при послідовному запуску. Причини:

    • Конкуренція за спільні дані. Два тести правлять того самого користувача чи той самий запис — гонка (race condition). Один читає те, що інший щойно змінив.
    • Спільні глобальні перемикачі. Тест вмикає на весь проєкт якийсь режим (наприклад, «обовʼязкове поле»), і паралельні сусіди, яким цей режим не потрібен, починають падати.
    • Обмежені ресурси. Один порт, один тимчасовий файл, одна квота на зовнішньому API — усі воркери бʼються за нього.
    • Порядок як прихована залежність. Послідовно тести випадково йшли в «правильному» порядку; паралель цей порядок ламає й оголює приховану звʼязаність.

    Ліки: справжня ізоляція даних (унікальні сутності на тест), уникнення глобальних мутацій спільного стану, а для тестів, які інакше не розчепити, — виділена серійна доріжка чи окремий проєкт/акаунт. Паралель — це лакмус: якщо вона валить тести, це майже завжди сигнал, що ізоляція вже дірява, просто послідовний прогін це маскував.

    Тротлінг мережі для лову race-багів

    Гонки (race conditions) — найпідступніші баги: на швидкій мережі порядок подій «випадково правильний», і дефект спить. Уповільнення мережі (network throttling) навмисно розтягує запити в часі й витягує назовні: подвійні сабміти, роботу з ще незавантаженими даними, стан «спінер завис назавжди».

    У Chromium через протокол DevTools (CDP) можна емулювати умови мережі:

    // Playwright + CDP: емуляція повільної мережі в Chromium
    const client = await context.newCDPSession(page);
    await client.send('Network.emulateNetworkConditions', {
      offline: false,
      downloadThroughput: 200 * 1024 / 8, // байти/с ≈ 200 кбіт/с
      uploadThroughput: 200 * 1024 / 8,
      latency: 400 // мс затримки на запит
    });

    Network.emulateNetworkConditions — це команда CDP (Chrome DevTools Protocol), тому вона працює лише в Chromium; для WebKit і Firefox через Playwright такого важеля немає. Значення downloadThroughput/uploadThroughput задають у байтах за секунду (-1 вимикає тротлінг), а latency — у мілісекундах.

    Простіший і кросбраузерний прийом — додати штучну затримку конкретному запиту через перехоплення:

    // Затримуємо саме потрібний запит, щоб оголити гонку
    await page.route('**/api/cart', async route => {
      await new Promise(r => setTimeout(r, 2000));
      await route.continue();
    });

    Це дозволяє, наприклад, натиснути «Оформити» двічі, поки перший запит «висить», і перевірити, чи застосунок блокує повторний сабміт. Так тест із інструмента підтвердження перетворюється на інструмент пошуку реальних race-багів.

    Алгоритм діагностики: хто винен — застосунок, тест чи середовище

    Коли тест червоний, головне питання — де саме баг. Ось послідовність, яка веде до відповіді без гадань.

    1. Прочитай помилку буквально. element intercepts pointer events — щось перекриває клік. Timeout waiting for selector — очікуваний елемент не зʼявився. expected 200, got 500 — впав бекенд. Тип помилки вже звужує коло.
    2. Подивись артефакти. Скріншот, відео, трасування (trace), логи мережі й консолі на момент падіння. Часто видно одразу: висить спінер, показано 500, зʼїхав лейаут.
    3. Відтвори локально в умовах CI. Той самий образ / headless / viewport / TZ. Якщо в CI-умовах локально стабільно зелено, а на CI червоно — підозра зміщується на середовище й ресурси.
    4. Прожени багато разів. Скрипт, що ганяє тест 20–50 разів. Стабільно червоний = детермінований баг (застосунку або тесту). Іноді червоний = флак (гонка або слабке очікування).
    5. Ізолюй. Прожени тест наодинці й у складі всього набору. Наодинці зелений, у наборі червоний — це протікання стану або конкуренція за паралель, тобто проблема ізоляції.
    6. Розділи гіпотези за симптомом:
    СимптомНайімовірніший винуватець
    Падає лише в паралелі / в наборіТест (ізоляція, спільний стан)
    Падає лише на CI, локально в тих самих умовах — ніСередовище (ресурси, TZ, viewport)
    Падає стабільно скрізь після зміни кодуЗастосунок (реальний баг або зміна поведінки)
    Падає раз на N без явної системиГонка + слабке очікування (тест)
    Помилка мережі від стороннього доменуЗовнішня залежність (мокати)

    Лише на CI

    Всюди

    Так, після зміни коду

    Раз на N

    Так

    Ні

    Тест червоний

    Прочитай помилку буквально

    Подивись артефакти:
    скрін, відео, trace

    Відтворюється локально
    в умовах CI?

    Середовище:
    ресурси, TZ, viewport

    Стабільно червоний?

    Застосунок:
    реальний баг

    Падає лише
    в наборі / паралелі?

    Тест:
    ізоляція, спільний стан

    Тест:
    гонка + слабке очікування

    Лише на CI

    Всюди

    Так, після зміни коду

    Раз на N

    Так

    Ні

    Тест червоний

    Прочитай помилку буквально

    Подивись артефакти:
    скрін, відео, trace

    Відтворюється локально
    в умовах CI?

    Середовище:
    ресурси, TZ, viewport

    Стабільно червоний?

    Застосунок:
    реальний баг

    Падає лише
    в наборі / паралелі?

    Тест:
    ізоляція, спільний стан

    Тест:
    гонка + слабке очікування

    1. Сформулюй висновок чесно. Якщо винен застосунок — це знайдений баг, тест має лишатися червоним і фіксувати регресію, а не «зеленитися» через force чи sleep. Якщо винен тест — лагодь синхронізацію та ізоляцію. Якщо середовище — вирівнюй його або фіксуй передумови. І поки причина не відтворена й не виміряна, чесне формулювання — «ще не встановлено», а не поспішний «root cause».

    Ця дисципліна — не бюрократія, а спосіб не витрачати дні на «полагоджений» флак, який повертається наступного тижня. Хороший AQA не заспокоюється на зеленому: він знає, чому зелено, і що саме доводить кожне очікування в його тесті.