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

    06 · Автоматизація: стратегія

    Паралелізація: воркери, шардінг, масштабування

    Зміст

    з тисячі E2E-тестів, яка йде послідовно, легко займає годину-півтори. Годину на кожен пуш, годину на кожен реліз, годину, поки розробник тримає в голові контекст свого PR і чекає зеленого світла. У якийсь момент команда просто перестає ганяти повну на кожну зміну — і автоматизація, за яку заплатили місяцями роботи, тихо перетворюється на nightly-ритуал, результати якого ніхто не читає до ранку. Паралелізація (parallel execution) — це те, що не дає цьому статися: вона перетворює годину на пʼять хвилин, не змінюючи жодного тесту по суті.

    Це глава-поглиблення — при першому проході теорії автоматизації її можна пропустити й повернутися, коли ваша сюїта реально почне впиратися в час. Але тема канонічна: саме тут зібрано, чому тести взагалі можна запускати паралельно, які це вимагає й чим доводиться платити. Інші глави (, тест-дані, CI) посилаються сюди за концепцією. На співбесіді на senior-AQA питання про паралельний запуск — не про --workers, а про те, чи розумієте ви, які властивості тестів роблять цей прапорець безпечним.

    Час прогону — метрика, яку видно

    Почнімо з того, навіщо взагалі паралелити. Автотести існують заради швидкого зворотного звʼязку (feedback loop): що раніше після зміни коду розробник дізнається, що щось зламав, то дешевше це полагодити. Довгий прогон бʼє по цьому напряму й тільки по ньому — тести від повільності не стають менш надійними, вони стають менш корисними, бо їх запускають рідше.

    Тут важливо розрізняти дві величини:

    • Сумарний час (total / CPU time) — скільки роботи виконано, якщо скласти тривалість усіх тестів. Паралелізація його не зменшує: та сама тисяча тестів виконує ту саму роботу.
    • Час за годинником (wall-clock time) — скільки реально минуло від старту прогону до вердикту. Саме його відчуває людина, і саме його стискає паралельність.

    Ідеальне прискорення лінійне: чотири — вчетверо швидше за годинником. На практиці так не буває, і причини цього — половина цієї глави. Але орієнтир простий: якщо ваша сюїта йде 40 хвилин на одному потоці, а могла б за пʼять на восьми — це втрачені 35 хвилин на кожному прогоні, помножені на кількість прогонів на день і на кількість інженерів, що чекають. Час прогону — метрика, яку варто моніторити нарівні з відсотком проходження й -рейтом (див. Звітність).

    Передумова, без якої нічого не працює: незалежність

    Паралельний запуск не має магії планувальника, яка «розрулить» конфлікти. Він просто бере ваші тести й пускає їх одночасно в різному, наперед невідомому порядку. Якщо тести до цього не готові — ви не отримаєте прискорення, ви отримаєте випадкові фейли.

    Тому єдина справжня передумова паралельності — ізоляція (isolation): кожен тест має виконуватися так, ніби він у системі один. Це те саме, що робить тест незалежним від порядку (order independence), і воно розкладається на два рівні.

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

    Незалежність даних. Це тонший рівень і найчастіша причина, чому «локально зелено, у паралелі — червоно». Два тести можуть бути бездоганно незалежні в коді й усе одно зчепитися через спільні дані на бекенді: обидва редагують профіль того самого користувача, обидва розраховують на порожній кошик, обидва створюють сутність з унікальним іменем test-order. У послідовному прогоні вони не перетинаються в часі й тому мовчать; у паралельному — стають одне для одного джерелом недетермінованих гонок (race conditions). Стратегія, яка це лікує (дані на тест, дані на воркер, фабрики замість спільних ), — предмет окремого канону Тест-дані: фабрики, фікстури, ізоляція; тут важливо запамʼятати сам принцип: паралельність не створює нових багів у тестах — вона проявляє ті, що вже були приховані порядком.

    Хороший фреймворк допомагає з ізоляцією браузерного рівня майже даром. У Playwright кожен воркер стартує зі свого browser context — фактично чистого профілю без спільних cookie, localStorage і кешу (див. Кукі, сесії та сховище браузера). Але жоден фреймворк не ізолює ваш бекенд — рядок у спільній базі даних воркери ділять незалежно від того, наскільки чисті їхні браузери.

    Воркери проти шардінгу

    Два слова, які плутають, бо обидва «про паралельність», — але це різні осі масштабування.

    Воркер (worker) — це окремий процес на одній машині, який тягне свою пачку тестів. піднімає кілька воркерів (у Playwright за замовчуванням — приблизно половину логічних ядер CPU), роздає їм тести й збирає результати в один звіт. Це паралельність усередині однієї машини, обмежена її ядрами й памʼяттю: восьмиядерний агент реально тягне десь 4–8 браузерних воркерів, далі вони починають конкурувати за CPU й лише сповільнюють одне одного.

    (sharding) — це розбиття сюїти на частини (шарди), які йдуть на різних машинах одночасно. Кожна машина бере свою частку — наприклад, --shard=1/4 означає «перша чверть тестів із чотирьох». Машини нічого не знають одна про одну; наприкінці їхні часткові звіти зливають в один (у Playwright — через blob-репортер і команду merge-reports).

    Машина 2 — shard 2/2

    Воркер 1

    Воркер 2

    Машина 1 — shard 1/2

    Воркер 1

    Воркер 2

    Сюїта: 400 тестів

    merge-reports → один звіт

    Машина 2 — shard 2/2

    Воркер 1

    Воркер 2

    Машина 1 — shard 1/2

    Воркер 1

    Воркер 2

    Сюїта: 400 тестів

    merge-reports → один звіт

    Осі перемножуються: 2 машини по 4 воркери = 8 тестів одночасно. Воркери масштабують вас до стелі однієї машини; шардінг пробиває цю стелю, докидаючи машини. Практичне правило: спершу вижміть воркери на одному агенті (це безкоштовно — ядра вже оплачені), і лише коли одна машина впирається, вмикайте шардінг у CI, де кожен шард — окрема паралельна . Механіка розкладки шардів по CI-агентах і її вартість у хвилинах — тема суміжного розділу про CI/CD; Selenium Grid як спосіб винести браузери на віддалені ноди — тема розділу про інструменти. Тут нам важлива концепція: масштабування має дві незалежні осі, і питання «воркери чи шардінг» — хибне, бо відповідь «і те, й те, в такому порядку».

    // Один агент: підняти вручну більше воркерів, ніж дефолт
    // npx playwright test --workers=6
    
    // Кілька агентів у CI: кожен бере свій шард
    // npx playwright test --shard=1/4   (машина 1)
    // npx playwright test --shard=2/4   (машина 2) ...

    Зауваження про гранулярність: паралельність між файлами фреймворки дають з коробки, а от тести в межах одного файлу Playwright за замовчуванням виконує послідовно. Щоб пустити паралельно й їх, файл вмикають у режим test.describe.configure({ mode: 'parallel' }) або задають fullyParallel: true глобально. Це важливо для балансування: файл-моноліт із 50 тестів стає неподільним блоком і псує розкладку.

    Спільні ресурси: один юзер = черга болю

    Найпоширеніша історія провалу паралелізації навіть не про код тестів — вона про архітектуру тестового оточення. Класика: на весь проєкт заведено один технічний акаунт, під яким логіняться всі тести. Послідовно це працює роками. Вмикаєш чотири воркери — і починається:

    • один тест змінює налаштування профілю, поки інший їх перевіряє;
    • один розлогінюється (інвалідує сесію), поки другий у середині свого сценарію під тією ж сесією;
    • два тести одночасно створюють «єдиний чернетковий документ» і ловлять конфлікт унікальності;
    • тест очікує «рівно 3 замовлення в списку», а сусід під тим самим юзером щойно створив четверте.

    Це і є «один юзер — черга болю»: спільний ресурс серіалізує те, що ви намагалися розпаралелити, або, гірше, породжує гонки, які виглядають як випадковий флак і зʼїдають дні на дебаг. Той самий клас проблем дає будь-який єдиний спільний ресурс: один рядок конфігу в БД, один зовнішній sandbox-акаунт платіжки з rate-лімітом, фіксований порт, який слухають усі воркери, одна тестова поштова скринька.

    Лікування концептуально одне — розшити спільне на приватне per-worker:

    • Користувач на воркер. Не один акаунт на всіх, а пул: воркер бере акаунт за своїм індексом і володіє ним одноосібно на весь прогон. Playwright дає стабільний індекс через process.env.TEST_PARALLEL_INDEX (0…workers−1) — зручний ключ у масив підготовлених юзерів.
    • Дані на тест. Усе, що тест створює, має бути унікальним: не test-order, а test-order-${Date.now()}-${randomId}. Тоді два воркери фізично не можуть зіткнутися на унікальному полі.
    • Ізольований простір. Де можливо — окрема схема/tenant/namespace на воркер, щоб «порожній кошик» одного не залежав від дій іншого.
    // Акаунт закріплений за воркером, а не спільний на всіх
    const users = [
      { email: 'runner-0@test.local', password: '...' },
      { email: 'runner-1@test.local', password: '...' },
      // ...по одному на кожен паралельний слот
    ];
    
    test.beforeEach(async ({ page }) => {
      const user = users[Number(process.env.TEST_PARALLEL_INDEX)];
      await loginAs(page, user);
    });

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

    Балансування

    Припустимо, ізоляція є, ресурси розшиті. Лишається розкласти тести по шардах так, щоб усі фінішували одночасно. Це і є балансування (load balancing) — і наївний спосіб «розділити порівну за кількістю» зазвичай не працює.

    Причина в тому, що тести дуже різні за тривалістю. Один E2E-сценарій оформлення замовлення йде 40 секунд, перевірка валідації рівня — 0,3 секунди. Розкладеш «по 100 тестів на шард» — і шард, куди випадково зібралися важкі сценарії, працюватиме вдвічі довше за сусіда. А час усього прогону дорівнює часу найдовшого шарда: решта машин уже простоюють, а ви все ще чекаєте на відсталого. Ми називаємо таку найдовшу неподільну одиницю «довгою жердиною» — і одразу застереження: це наша робоча назва, а не галузевий термін. Усталеної англійської назви для неї в наших джерелах немає (long pole там не трапляється жодного разу), тож на співбесіді надійніше описати механізм, ніж кинути слово.

    За історією тривалості — рівно

    Shard 1: 5.5 хв ▓▓▓▓▓

    Shard 2: 5.5 хв ▓▓▓▓▓

    Порівну за кількістю — дисбаланс

    Shard 1: 8 хв ▓▓▓▓▓▓▓▓

    Shard 2: 3 хв ▓▓▓

    Прогін = 8 хв

    Прогін = 5.5 хв

    За історією тривалості — рівно

    Shard 1: 5.5 хв ▓▓▓▓▓

    Shard 2: 5.5 хв ▓▓▓▓▓

    Порівну за кількістю — дисбаланс

    Shard 1: 8 хв ▓▓▓▓▓▓▓▓

    Shard 2: 3 хв ▓▓▓

    Прогін = 8 хв

    Прогін = 5.5 хв

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

    Практичні наслідки для AQA:

    • Гранулярність — ваш друг. Дрібніші одиниці розкладаються рівніше. Файл-моноліт із 50 тестів, який іде послідовно, — неподільна «жердина»; розбийте його або дозвольте паралельність усередині.
    • Один патологічно повільний тест ламає всю картину. Якщо один сценарій іде 6 хвилин, жоден шардінг не опустить прогін нижче цих 6 хвилин. Іноді дешевше оптимізувати цей один тест, ніж докидати машини.
    • Не покладайтеся на порядок. Балансувальник розкладе тести як завгодно — це ще один аргумент за незалежність.

    Ціна паралелізації

    Паралельність — не безкоштовний обід, і senior відрізняється від junior тим, що бачить рахунок.

    Інфраструктура. Кожен шард — це окрема CI-джоба, тобто окрема машина й окремі оплачувані хвилини. Восьмиразове прискорення за годинником зазвичай означає приблизно восьмиразову витрату машино-хвилин (сумарна робота нікуди не поділася, ви лише робите її одночасно). Для хмарного CI з поштучною тарифікацією це прямі гроші, для self-hosted — зайняті ранери, яких може бракувати іншим командам.

    Флак виходить на поверхню. Паралельний запуск створює конкуренцію за CPU, памʼять, мережу й спільні ресурси — саме ті умови, у яких вилазять гонки й , невидимі на спокійному послідовному прогоні. Тому вмикання паралельності часто піднімає флакі-рейт, і це не регрес, а викриття давно наявних проблем ізоляції. Розбір і лікування — у главі Флакі-тести.

    Дебаг ускладнюється. Логи трьох воркерів переплетені, «впав тест X» тепер залежить від того, що робив у той момент воркер Y, а відтворити конкретну розкладку локально важче. Хороші звіти (, відео, ізольовані по тесту логи) з паралельністю стають не розкішшю, а необхідністю.

    Спадна віддача. Прискорення обмежене послідовною часткою роботи — це загальний закон паралельних систем (, Amdahl's law), сформульований Джином Амдалом на конференції AFIPS 1967 року: «even with an infinite number of processors, the speedup is constrained by the unparallelizable portion». Масштаб ефекту видно на числах: якщо паралелиться рівно половина роботи, максимум прискорення — вдвічі; якщо 95% — у двадцять разів. Стосовно сюїти це означає: якщо частину доводиться серіалізувати (спільний ресурс, який не розшити), то хоч сто машин докинь — нижче цієї межі прогін не впаде.

    Одне розведення, щоб не зліплювати два різні твердження. «Час прогону дорівнює найдовшому шарду» із закону Амдала не виводиться — це властивість розкладки роботи, і тримається вона на доці ранера, а не на Амдалі. Закон обмежує прискорення від додавання ресурсів; жердина обмежує годинниковий час. Обидва обмеження реальні, але це різні обмеження. Плюс фіксовані накладні витрати: кожен воркер піднімає браузер, кожен шард піднімає оточення й тягне залежності. На дрібній сюїті ці накладні можуть зʼїсти весь виграш — 30-секундний набір із паралельністю піде повільніше, ніж без неї.

    Звідси інженерне рішення: паралелити треба рівно стільки, скільки окупається. Орієнтир — вирівнювати кількість воркерів/шардів так, щоб прогін входив у прийнятне вікно зворотного звʼязку (наприклад, PR-сюїта — до 10 хвилин), не докидаючи потужність заради потужності.

    Типові помилки

    • Виглядає як «увімкнув --workers=8 і став швидший», а насправді сюїта почала червоніти випадково. Паралельність не додала багів — вона проявила залежності через спільні дані, які приховував послідовний порядок. Лікують не відкатом воркерів, а ізоляцією.
    • Виглядає як незалежні тести, а насправді вони ділять одного тест-юзера. Код бездоганний, браузерні контексти чисті — але стан на бекенді один на всіх. Класична «черга болю»: розшивати на пул акаунтів per-worker.
    • Виглядає як рівний розподіл «по 100 тестів на шард», а насправді один шард удвічі довший. Балансувати треба за тривалістю, а не за кількістю: час прогону дорівнює часу найдовшого шарда.
    • Виглядає як «докинемо ще машин — стане ще швидше», а насправді впираємося в одну повільну жердину. Один 6-хвилинний тест або серіалізований блок ставить нижню межу, яку не пробити кількістю машин (закон Амдала).
    • Виглядає як економія, а насправді CI-рахунок зріс увосьмеро. Прискорення за годинником не зменшує сумарну роботу — воно розкладає її на паралельні оплачувані хвилини.
    • Виглядає як «мій фреймворк ізолює тести», а насправді він ізолює лише браузер. Чистий browser context не рятує від конфлікту двох воркерів за рядок у спільній БД — ізоляція даних на вашій відповідальності.

    Підсумок

    • Мета паралелізації — стиснути час за годинником (feedback loop), а не сумарну роботу; це метрика, яку моніторять нарівні з проходженням і флаком.
    • Єдина справжня передумова — ізоляція: незалежність від порядку в коді і незалежність даних на бекенді. Паралельність не створює нові баги в тестах, вона проявляє приховані порядком.
    • Воркери масштабують усередині однієї машини (до її ядер), шардінг — між машинами; осі перемножуються, тож спершу воркери, потім шарди.
    • Спільний ресурс (один юзер, один рядок, один порт) серіалізує паралельне або породжує гонки; лікування — розшити на приватне per-worker або зробити дані унікальними.
    • Балансувати шарди треба за тривалістю, а не за кількістю; прогін дорівнює найдовшому шарду. І памʼятайте про ціну: інфраструктуру, виявлений флак, складніший дебаг і спадну віддачу за законом Амдала.

    Можливі питання

    • «Ваша регресія йде годину. Ваші дії?» — інтервʼюер чекає не одного трюку, а системи: спершу воркери на одному агенті, потім шардінг у CI, паралельно — ревізія ізоляції й пошук повільних «жердин». Слабка відповідь — «поставлю --workers=10»; сильна починає з питання «а тести незалежні?».
    • «Що має бути істинним про тести, щоб їх можна було запускати паралельно?» — ядро теми. Очікують «незалежність від порядку» плюс незалежність даних, і приклад спільного ресурсу, що ламає паралель (один тест-юзер, спільний рядок у БД).
    • «Чим воркери відрізняються від шардінгу?» — перевірка, чи не каша в голові: воркери — процеси на одній машині (ліміт — ядра), шардінг — розбиття між машинами (ліміт — бюджет). Бонус — що вони перемножуються.
    • «Увімкнули паралельність — посипалися випадкові фейли. Чому й що робити?» — дивляться, чи розумієте ви, що це прояв поганої ізоляції, а не «баг паралельності». Правильний хід — знайти спільний стан, а не вимкнути паралель.
    • «Як розкласти тести по десятьох агентах, щоб було швидко?» — сигнал на балансування: за історією тривалості, а не за кількістю; згадка про «довгу жердину» й гранулярність (розбити файли-моноліти).
    • «Скільки коштує паралелізація?» — питання на зрілість: сумарні машино-хвилини не зменшуються, флак виходить на поверхню, дебаг важчає, віддача спадна (Амдал). Кандидат, що бачить рахунок, — senior.

    Джерела

    Час прогону — метрика, яку видно

    • ISTQB® CTAL-TAE Syllabus v2.0 — §1.1: автоматизація дає те, чого руками зробити неможливо взагалі, і паралельне виконання названо серед прикладів; там же — швидший фідбек про якість SUT як окрема вигода.
    • ISTQB® CTFL Syllabus v4.0.1 — §6.2: серед вигід засобів тестування — доступ до інформації для управління тестуванням, зокрема статистики про тривалість і частоту падінь.

    Передумова, без якої нічого не працює: незалежність

    • Playwright — Test Isolation (browser contexts) — ізоляція знімає питання порядку виконання, і саме це робить паралельність та шардінг можливими; серед названих наслідків — «не треба думати про порядок».
    • Playwright — Parallelism — межа ізоляції: воркер це процес ОС зі своїм браузером, спільного стану й глобальних змінних між воркерами немає — але про бекенд дока нічого не обіцяє.
    • xUnit Test Patterns — Fresh Fixture — те, що лишається на вашій відповідальності: свіжа фікстура на кожен тест запобігає нестабільним тестам, а спільна робить «війну прогонів» можливою.

    Воркери проти шардінгу

    • Playwright — Parallelism — одиниця паралельності за замовчуванням — файл, а не тест; тести всередині файлу йдуть по черзі в одному воркері, поки це явно не змінили, а послідовний режим (serial) дока прямо називає не рекомендованим: падіння одного пропускає наступні, а іде на всю групу.
    • Playwright — Sharding — інший вимір розподілу: --shard=index/total ділить сюїту між машинами, а часткові звіти зливають blob-репортером через merge-reports.
    • CodeceptJS — Parallel Execution — інша механіка того самого: воркер може бути потоком із незалежним екземпляром інструмента, а не процесом ОС; побічний наслідок названо прямо — покрокового виводу в більше немає.

    Спільні ресурси: один юзер = черга болю

    • xUnit Test Patterns — Erratic Test — точний діагноз конфлікту за спільний ресурс: «війна прогонів» можлива тільки за глобально спільної фікстури, до якої різні тести звертаються й іноді її змінюють.
    • Playwright — Test fixtures — штатний спосіб зробити ресурс приватним для воркера: воркерна фікстура створюється раз на воркер і живе, поки живий воркер.
    • Playwright — Parallelism — і чому «просто вимкнути паралель» не безкоштовно: вимкнення = обмежити воркерів до одного, окремого прапорця немає.

    Балансування

    • CodeceptJS — Parallel Execution — три стратегії розподілу з умовами застосування: test (розподіл наперед — коли тести приблизно однакові за часом), suite (сюїта не розривається — коли є спільний BeforeSuite), pool (черга — коли тривалості різні).
    • Playwright — Sharding — механіка розкладки по машинах і злиття часткових звітів, без якої балансування не видно у звіті.
    • Wikipedia — Amdahlʼs law — чому найдовший тест ставить підлогу: «even with an infinite number of processors, the speedup is constrained by the unparallelizable portion» (trust: secondary, dec-0710).

    Ціна паралелізації

    • Wikipedia — Amdahlʼs law — формула, що обмежує прискорення при додаванні ресурсів (Джин Амдал, AFIPS 1967), і масштаб ефекту: паралелиться 50 % роботи — максимум ×2, 95 % — ×20; джерело саме звʼязує це зі спадною віддачею (trust: secondary, dec-0710).
    • Playwright — Parallelism — прихована стаття витрат: після падіння тесту воркер гарантовано вбивається, щоб наступні стартували в чистому оточенні, а в повністю паралельному режимі beforeAll/afterAll виконуються для кожного тесту окремо.
    • ISTQB® CTAL-TAE Syllabus v2.0 — §1.1: у переліку недоліків автоматизації прямо стоять початкова інвестиція та тривала вартість — саме та рамка, у якій рахують окупність воркерів.

    Пояснення

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

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

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