Паралелізація: воркери, шардінг, масштабування
Зміст
Сьюта з тисячі 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 машини по 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): решта машин уже простоюють, а ви все ще чекаєте на відсталого.
Правильний підхід — балансувати за очікуваною тривалістю, а не за кількістю. Для цього потрібні дані з попередніх прогонів: скільки насправді йшов кожен тест. Розвинені платформи (у тому числі хмарні сервіси для запуску тестів) роблять це автоматично — тримають історію таймінгів і розкладають тести жадібно, кладучи наступний найдовший тест у шард, що зараз найпорожніший. Всередині однієї машини ранер балансує сам: воркер, що звільнився, одразу бере наступний тест із черги, тож простій між воркерами мінімальний — дисбаланс болить саме на рівні шардів, які нарізають наперед.
Практичні наслідки для AQA:
- Гранулярність — ваш друг. Дрібніші одиниці розкладаються рівніше. Файл-моноліт із 50 тестів, який іде послідовно, — неподільна «жердина»; розбийте його або дозвольте паралельність усередині.
- Один патологічно повільний тест ламає всю картину. Якщо один сценарій іде 6 хвилин, жоден шардінг не опустить прогін нижче цих 6 хвилин. Іноді дешевше оптимізувати цей один тест, ніж докидати машини.
- Не покладайтеся на порядок. Балансувальник розкладе тести як завгодно — це ще один аргумент за незалежність.
Ціна паралелізації
Паралельність — не безкоштовний обід, і senior відрізняється від junior тим, що бачить рахунок.
Інфраструктура. Кожен шард — це окрема CI-джоба, тобто окрема машина й окремі оплачувані хвилини. Восьмиразове прискорення за годинником зазвичай означає приблизно восьмиразову витрату машино-хвилин (сумарна робота нікуди не поділася, ви лише робите її одночасно). Для хмарного CI з поштучною тарифікацією це прямі гроші, для self-hosted — зайняті раннери, яких може бракувати іншим командам.
Флак виходить на поверхню. Паралельний запуск створює конкуренцію за CPU, памʼять, мережу й спільні ресурси — саме ті умови, у яких вилазять гонки й таймаути, невидимі на спокійному послідовному прогоні. Тому вмикання паралельності часто піднімає флакі-рейт, і це не регрес, а викриття давно наявних проблем ізоляції. Розбір і лікування — у главі Флакі-тести.
Дебаг ускладнюється. Логи трьох воркерів переплетені, «впав тест X» тепер залежить від того, що робив у той момент воркер Y, а відтворити конкретну розкладку локально важче. Хороші звіти (трейси, відео, ізольовані по тесту логи) з паралельністю стають не розкішшю, а необхідністю.
Спадна віддача. Прискорення обмежене послідовною часткою роботи — це загальний закон паралельних систем (закон Амдала, Amdahl's law): якщо частину сьюти доводиться серіалізувати (спільний ресурс, який не розшити) або одна «жердина» триває 5 хвилин, то хоч сто машин докинь — нижче цієї межі прогін не впаде. Плюс фіксовані накладні витрати: кожен воркер піднімає браузер, кожен шард піднімає оточення й тягне залежності. На дрібній сьюті ці накладні можуть зʼїсти весь виграш — 30-секундний набір із паралельністю піде повільніше, ніж без неї.
Звідси інженерне рішення: паралелити треба рівно стільки, скільки окупається. Орієнтир — вирівнювати кількість воркерів/шардів так, щоб прогін входив у прийнятне вікно зворотного звʼязку (наприклад, PR-сьюта — до 10 хвилин), не докидаючи потужність заради потужності.
Типові помилки
- Виглядає як «увімкнув
--workers=8і став швидший», а насправді сьюта почала червоніти випадково. Паралельність не додала багів — вона проявила залежності через спільні дані, які приховував послідовний порядок. Лікують не відкатом воркерів, а ізоляцією. - Виглядає як незалежні тести, а насправді вони ділять одного тест-юзера. Код бездоганний, браузерні контексти чисті — але стан на бекенді один на всіх. Класична «черга болю»: розшивати на пул акаунтів per-worker.
- Виглядає як рівний розподіл «по 100 тестів на шард», а насправді один шард удвічі довший. Балансувати треба за тривалістю, а не за кількістю: час прогону дорівнює часу найдовшого шарда.
- Виглядає як «докинемо ще машин — стане ще швидше», а насправді впираємося в одну повільну жердину. Один 6-хвилинний тест або серіалізований блок ставить нижню межу, яку не пробити кількістю машин (закон Амдала).
- Виглядає як економія, а насправді CI-рахунок зріс увосьмеро. Прискорення за годинником не зменшує сумарну роботу — воно розкладає її на паралельні оплачувані хвилини.
- Виглядає як «мій фреймворк ізолює тести», а насправді він ізолює лише браузер. Чистий
browser contextне рятує від конфлікту двох воркерів за рядок у спільній БД — ізоляція даних на вашій відповідальності.
Підсумок
- Мета паралелізації — стиснути час за годинником (feedback loop), а не сумарну роботу; це метрика, яку моніторять нарівні з проходженням і флаком.
- Єдина справжня передумова — ізоляція: незалежність від порядку в коді і незалежність даних на бекенді. Паралельність не створює нові баги в тестах, вона проявляє приховані порядком.
- Воркери масштабують усередині однієї машини (до її ядер), шардінг — між машинами; осі перемножуються, тож спершу воркери, потім шарди.
- Спільний ресурс (один юзер, один рядок, один порт) серіалізує паралельне або породжує гонки; лікування — розшити на приватне per-worker або зробити дані унікальними.
- Балансувати шарди треба за тривалістю, а не за кількістю; прогін дорівнює найдовшому шарду. І памʼятайте про ціну: інфраструктуру, виявлений флак, складніший дебаг і спадну віддачу за законом Амдала.
Що питають на співбесіді
- «Ваша регресія йде годину. Ваші дії?» — інтервʼюер чекає не одного трюку, а системи: спершу воркери на одному агенті, потім шардінг у CI, паралельно — ревізія ізоляції й пошук повільних «жердин». Слабка відповідь — «поставлю
--workers=10»; сильна починає з питання «а тести незалежні?». - «Що має бути істинним про тести, щоб їх можна було запускати паралельно?» — ядро теми. Очікують «незалежність від порядку» плюс незалежність даних, і приклад спільного ресурсу, що ламає паралель (один тест-юзер, спільний рядок у БД).
- «Чим воркери відрізняються від шардінгу?» — перевірка, чи не каша в голові: воркери — процеси на одній машині (ліміт — ядра), шардінг — розбиття між машинами (ліміт — бюджет). Бонус — що вони перемножуються.
- «Увімкнули паралельність — посипалися випадкові фейли. Чому й що робити?» — дивляться, чи розумієте ви, що це прояв поганої ізоляції, а не «баг паралельності». Правильний хід — знайти спільний стан, а не вимкнути паралель.
- «Як розкласти тести по десятьох агентах, щоб було швидко?» — сигнал на балансування: за історією тривалості, а не за кількістю; згадка про «довгу жердину» й гранулярність (розбити файли-моноліти).
- «Скільки коштує паралелізація?» — питання на зрілість: сумарні машино-хвилини не зменшуються, флак виходить на поверхню, дебаг важчає, віддача спадна (Амдал). Кандидат, що бачить рахунок, — senior.
Джерела
- ISTQB Certified Tester Foundation Level (CTFL) 4.0 Syllabus — розділ про засоби тестування (Test Tools): переваги й ризики автоматизації та супровід тестового забезпечення (testware); повторюваність і консистентність виконання як переваги автоматизованих тестів.
- Playwright — Parallelism — воркери,
fullyParallel, режимparallelу межах файлу, змінніTEST_PARALLEL_INDEX/TEST_WORKER_INDEX. - Playwright — Sharding —
--shard=index/total, злиття часткових звітів черезblob-репортер іmerge-reports. - Playwright — Test isolation — ізоляція стану через
browser contextяк основа безпечної паралельності на рівні браузера. - CodeceptJS — Parallel Execution (run-workers) — запуск сьюти воркерами й розподіл тестів між ними в CodeceptJS.
Навіщо взагалі паралелити тести й яку саме величину це прискорює?
Паралелізація стискає час за годинником (wall-clock) — скільки реально минуло від старту прогону до вердикту, — а не сумарну роботу. Механізм простий: та сама тисяча тестів виконує ту саму кількість роботи, але воркери й машини женуть її одночасно, тож людина отримує результат раніше. Практичний наслідок — коротший цикл зворотного звʼязку (feedback loop): розробник швидше дізнається, що зламав, і команда не скочується до nightly-регресії, яку до ранку ніхто не читає. Тому час прогону варто тримати під моніторингом нарівні з відсотком проходження й флакі-рейтом.
Чим сумарний час відрізняється від часу за годинником?
Сумарний час (total / CPU time) — це сума тривалостей усіх тестів, тобто скільки роботи зроблено загалом. Час за годинником — скільки реально пройшло від запуску до результату. Паралельність не чіпає перше й стискає друге: восьмиразове прискорення за годинником не робить сьюту «легшою», сумарна робота лишається тією ж, просто розкладеною одночасно. Це важливо памʼятати, коли рахуєш ціну: машино-хвилини привʼязані саме до сумарного часу, і він нікуди не зникає.
Що має бути істинним про тести, щоб їх можна було запускати паралельно?
Ключова властивість — ізоляція, вона ж незалежність від порядку (order independence). Розкладається на два рівні: незалежність коду (тест не читає стан, який лишив сусід, сам готує все у фазі Arrange і сам прибирає) і незалежність даних (тест не бʼється з іншими за спільний рядок на бекенді). Плюс відсутність спільного ресурсу, який доводиться серіалізувати або який породжує гонки. Слабка відповідь зупиняється на «код незалежний»; сильна одразу згадує дані й наводить приклад спільного ресурсу — один тест-юзер чи спільний рядок у БД.
Чим воркери відрізняються від шардінгу?
Це дві різні осі масштабування, які легко переплутати. Воркер (worker) — окремий процес на одній машині, обмежений її ядрами й памʼяттю. Шардінг (sharding) — розбиття сьюти на частини, що йдуть на різних машинах, обмежене бюджетом на ці машини. Осі незалежні й перемножуються: дві машини по чотири воркери дають вісім тестів одночасно. Практичне правило — спершу вижати воркери (ядра вже оплачені), і лише коли одна машина впирається, вмикати шардінг; питання «воркери чи шардінг» хибне, бо відповідь «і те, й те, у цьому порядку».
Тест зелений локально, а в паралелі червоний. Що сталося?
Найчастіше це прихована залежність через спільні дані на бекенді. У послідовному прогоні тести не перетинаються в часі й тому мовчать; коли їх пускають одночасно, вони стають одне для одного джерелом недетермінованих гонок (race conditions). Головна теза: паралельність не додала жодного нового бага — вона проявила ті, що вже жили в сьюті, приховані порядком запуску. Тому лікують не відкатом воркерів, а ізоляцією даних: унікальні значення на тест, приватні ресурси на воркер.
Що таке «один юзер — черга болю»?
Це класична історія провалу: на весь проєкт заведено один технічний акаунт, під яким логіняться всі тести. Послідовно це працює роками, але під кількома воркерами починаються гонки — один тест міняє налаштування профілю, поки інший їх перевіряє; один розлогінюється посеред чужого сценарію; двоє одночасно створюють «єдиний чернетковий документ» і ловлять конфлікт унікальності; тест чекає «рівно три замовлення», а сусід під тим самим юзером щойно створив четверте. Спільний ресурс або серіалізує те, що ти намагався розпаралелити, або дає флак, що зʼїдає дні дебагу. Лікування — розшити спільне на приватне per-worker: пул акаунтів, де воркер володіє своїм одноосібно.
Як у Playwright закріпити ресурс за конкретним воркером?
Через стабільний індекс воркера — змінну TEST_PARALLEL_INDEX (значення від 0 до кількості воркерів мінус один), якою беруть елемент із заздалегідь підготовленого масиву. Кожен воркер бере акаунт за своїм індексом і володіє ним одноосібно весь прогін, тож двоє фізично не логіняться під одним юзером. Так само розшивають будь-який пул — порт, tenant, поштову скриньку. А те, що не можна ані розмножити, ані зробити приватним, доводиться серіалізувати — і кожен такий серіалізований шматок бʼє по прискоренню.
Чому балансувати шарди треба за тривалістю, а не за кількістю?
Тому що тести дуже різні за часом: E2E-сценарій оформлення замовлення йде десятки секунд, а юніт-подібна перевірка валідації — частки секунди. Розкладеш «по сто тестів на шард» — і шард, куди випадково зібралися важкі сценарії, працюватиме вдвічі довше за сусіда. А час усього прогону дорівнює часу найдовшого шарда: решта машин уже простоюють, а ти все ще чекаєш на відсталого. Правильний підхід — розкладати за історією таймінгів, жадібно кладучи наступний найдовший тест у той шард, що зараз найпорожніший.
Що таке «довга жердина» (long pole) і чому вона обмежує прискорення?
Жердина — це найдовша неподільна одиниця роботи, під яку підлаштовується весь прогін. Один тест на шість хвилин або серіалізований блок ставлять нижню межу, яку не пробити кількістю машин: хоч сто шардів докинь, прогін не впаде нижче цих шести хвилин. Наслідок для AQA — іноді дешевше оптимізувати один патологічно повільний тест, ніж докидати інфраструктуру. Файл-моноліт із пʼятдесяти послідовних тестів — теж жердина, бо стає неподільним блоком.
Чому Playwright за замовчуванням не паралелить тести в межах одного файлу і як це змінити?
Паралельність між файлами фреймворк дає з коробки, а от тести всередині одного файлу за замовчуванням виконує послідовно — щоб уникнути несподіванок зі спільним станом усередині файлу. Щоб пустити паралельно й їх, файл вмикають у режим test.describe.configure({ mode: 'parallel' }) або задають fullyParallel: true глобально. Це прямо впливає на балансування: неподільний файл-моноліт псує розкладку, ставши однією довгою жердиною, тож дрібніша гранулярність рівніше розкладається по шардах.
Скільки коштує паралелізація?
Прискорення за годинником не зменшує сумарної роботи — воно лише розкладає її на паралельні оплачувані машино-хвилини. Восьмиразове прискорення означає приблизно восьмиразову витрату машино-хвилин: для хмарного CI з поштучною тарифікацією це прямі гроші, для self-hosted — зайняті раннери, яких може бракувати іншим командам. Плюс приховані рахунки: флак виходить на поверхню, дебаг важчає через переплетені логи воркерів, а кожен воркер і шард додає фіксовані накладні на підняття браузера й оточення. Кандидат, що бачить цей рахунок, а не лише виграш у хвилинах, — senior.
Що таке закон Амдала стосовно тестової сьюти?
Закон Амдала (Amdahl's law) каже, що прискорення паралельної системи обмежене її послідовною часткою. Стосовно сьюти це означає: якщо частину тестів доводиться серіалізувати (спільний ресурс, який не розшити) або одна жердина триває пʼять хвилин, то хоч скільки машин докидай — нижче цієї межі прогін не впаде. До того ж кожен воркер і шард додає фіксовані накладні на старт. Практичний висновок — на дрібній сьюті ці накладні можуть зʼїсти весь виграш: тридцятисекундний набір під паралеллю піде повільніше, ніж без неї.
Чому «мій фреймворк ізолює тести» — небезпечна ілюзія?
Бо хороший фреймворк ізолює лише браузерний рівень, а не весь стан. У Playwright кожен воркер стартує зі свіжого browser context — фактично чистого профілю без спільних cookie, localStorage і кешу, тож на рівні браузера тести не заважають одне одному майже даром. Але жоден фреймворк не ізолює твій бекенд: рядок у спільній базі даних воркери ділять незалежно від того, наскільки чисті їхні браузери. Тому ізоляція даних лишається на твоїй відповідальності — і саме там ховається більшість гонок.
Ваша регресія йде годину. Ваші дії?
Інтервʼюер чекає систему, а не один трюк. Спершу вижати воркери на одному агенті (це безкоштовно — ядра вже оплачені), потім увімкнути шардінг у CI окремими паралельними джобами. Паралельно з цим — ревізія ізоляції: чи тести незалежні за кодом і за даними, бо без цього паралель дасть не швидкість, а випадкові фейли. І пошук повільних жердин, бо один шестихвилинний тест зведе весь шардінг нанівець. Слабка відповідь — «поставлю --workers=10»; сильна починає з питання «а тести взагалі незалежні?».
Як зібрати єдиний звіт із кількох шардів у Playwright?
Під час прогону машини нічого не знають одна про одну — кожна пише лише свій частковий результат. У Playwright для зведення є blob-репортер: кожен шард віддає blob-звіт, а наприкінці команда merge-reports зливає їх в один підсумковий (наприклад, HTML). Без цього кроку ти отримаєш кілька розрізнених звітів і не побачиш загальної картини проходження по всій сьюті. Тобто шардінг — це не лише розбиття запуску, а й обовʼязкове злиття результатів.
Скільки воркерів піднімати на одній машині й чому не «якомога більше»?
Орієнтир — приблизно половина логічних ядер (саме такий дефолт у Playwright): восьмиядерний агент реально тягне десь чотири–вісім браузерних воркерів. Далі вони починають конкурувати за CPU, памʼять і мережу й лише сповільнюють одне одного — прискорення давно не лінійне. Тому паралелити треба рівно стільки, скільки окупається: ціль — вкластися в прийнятне вікно зворотного звʼязку (наприклад, PR-сьюта до десяти хвилин), а не докидати потужність заради потужності.
Три кейси, у яких паралельність вирішує, зелена сьюта чи полювання за примарою: діагностика «локально зелено — у CI під воркерами червоно», розшивання спільного акаунта на пул per-worker у Playwright і розкладка сьюти по шардах у CI зі злиттям звітів. Скрізь — що дивитися й чому.
Кейс 1. «Локально зелено, у CI під воркерами червоно»: діагностика
Сьюта роками бігала послідовно й була стабільна. Хтось увімкнув --workers=4 у CI — і почалися плавучі падіння: щоразу червоніє інший тест, локально жоден не відтворюється. Перша спокуса — відкотити воркери. Правильний хід — прочитати симптом, бо паралель нічого не зламала, вона проявила приховану залежність.
| Симптом у CI | Найімовірніший корінь | Лікування |
|---|---|---|
| Щоразу падає різний тест, локально все зелене | Гонка за спільний стан, невидима послідовно | Ізолювати дані/ресурс per-worker |
409 Conflict на другому прогоні поспіль | Фіксовані дані не прибрані, дубль унікального поля | Унікальні значення на кожен тест |
| «Очікую 3 записи — приходить 4» | Сусідній воркер створив запис під тим самим юзером | Пул акаунтів per-worker |
| Тест логіниться, а сесія раптом невалідна | Інший тест розлогінив спільний акаунт | Приватний акаунт на воркер |
Дуже частий першоджерельний антипатерн — один тест-юзер на всіх плюс перевірка «рівно стільки записів»:
// АНТИПАТЕРН: усі воркери під одним акаунтом
test.beforeEach(async ({ page }) => {
await loginAs(page, { email: 'qa@test.local', password: '...' });
});
test('у списку рівно 3 замовлення', async ({ page }) => {
// сусідній воркер під тим самим юзером щойно створив 4-те
await expect(page.getByRole('row')).toHaveCount(3);
});
Що дивитися й чому:
- Відтворюваність — головний сигнал. Падає лише під паралеллю й не відтворюється поодинці — це майже завжди ізоляція, а не баг продукту. Не заводьте дефект на застосунок, поки не звузили до спільного ресурсу.
- Чистий браузер не рятує. Кожен воркер Playwright стартує зі свіжого
browser context, але спільний рядок на бекенді вони все одно ділять — браузерна ізоляція тут ні до чого. - Асерт на точну кількість під спільним юзером — крихкий за побудовою.
toHaveCount(3)істинний лише доти, доки ніхто інший не пише в той самий список.
Кейс 2. Playwright: розшити спільний акаунт на пул per-worker
Лікування «черги болю» концептуально одне — зробити спільний ресурс приватним для воркера. Стабільний ключ дає TEST_PARALLEL_INDEX (від 0 до кількості воркерів мінус один): за ним воркер бере свій акаунт із масиву й володіє ним весь прогін.
import { test, expect } from '@playwright/test';
// по одному акаунту на кожен паралельний слот
const users = [
{ email: 'runner-0@test.local', password: '...' },
{ email: 'runner-1@test.local', password: '...' },
{ email: 'runner-2@test.local', password: '...' },
{ email: 'runner-3@test.local', password: '...' },
];
test.beforeEach(async ({ page }) => {
const user = users[Number(process.env.TEST_PARALLEL_INDEX)];
await loginAs(page, user); // воркер логіниться під СВОЇМ юзером
});
Другий рівень — усе, що тест створює, має бути унікальним, щоб два воркери фізично не зіткнулися на унікальному полі:
test('чернетка створюється без конфлікту унікальності', async ({ page }) => {
// не 'test-order', а гарантовано унікальне імʼя
const name = `test-order-${Date.now()}-${crypto.randomUUID()}`;
await createOrder(page, { name });
await expect(page.getByText(name)).toBeVisible();
});
Що дивитися й чому:
- Пул має покривати всі слоти. Якщо воркерів більше, ніж акаунтів у масиві,
TEST_PARALLEL_INDEXвийде за межі й дастьundefined— тримайте розмір пулу не меншим за максимум воркерів. - Унікальність замінює прибирання там, де прибирати дорого. Таймстемп плюс випадковий ідентифікатор знімають конфлікт без складного teardown, а
409на повторному прогоні зникає сам. - Що не розмножити й не зробити приватним — серіалізуйте. Єдиний зовнішній sandbox платіжки з rate-лімітом іноді доведеться ганяти в один потік; памʼятайте, що кожен такий блок — це нижня межа за законом Амдала.
Кейс 3. Шардінг у CI зі злиттям звітів
Одна машина впирається в ядра — час пробивати стелю шардінгом. Кожен шард стає окремою паралельною джобою, а всередині кожної працюють ще й воркери: осі перемножуються.
# GitHub Actions: 4 шарди як паралельні джоби, всередині — по 4 воркери
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4 --workers=4 --reporter=blob
Машини нічого не знають одна про одну, тож наприкінці їхні часткові blob-звіти треба зібрати в один:
# зібрати blob-звіти всіх шардів і злити в єдиний HTML
npx playwright merge-reports --reporter=html ./all-blob-reports
Коли яку вісь вмикати:
| Ситуація | Вісь | Чому |
|---|---|---|
| Сьюта повільна, машина недозавантажена | Воркери | Ядра вже оплачені — прискорення безкоштовне |
| Воркери на межі ядер, час усе одно великий | Шардінг | Стелю однієї машини пробивають лише новими машинами |
| Один тест іде 6 хвилин | Ні те, ні те | Це жердина — оптимізуйте сам тест, шардінг її не поділить |
| Дрібна сьюта на 30 секунд | Нічого | Накладні на підняття браузерів зʼїдять виграш |
Що дивитися й чому:
- Балансуйте за тривалістю, а не за кількістю.
--shard=index/totalділить наївно; якщо один шард стабільно найдовший, перенесіть у нього менше важких сценаріїв або дозвольте паралельність усередині файлів-монолітів. - Прогін дорівнює найдовшому шарду. Дивіться не на середній час джоб, а на максимальний — саме він тримає весь пайплайн.
- Без
merge-reportsнемає загальної картини. Чотири розрізнені звіти не дадуть єдиного pass/fail; злиття — обовʼязковий крок шардінгу, а не опція.
Метрика і мета
- Розумію різницю між сумарним часом (total / CPU time) і часом за годинником (wall-clock) і що паралельність стискає лише друге.
- Можу пояснити, чому час прогону — метрика для моніторингу нарівні з відсотком проходження й флакі-рейтом: він бʼє прямо по циклу зворотного звʼязку.
Ізоляція — єдина справжня передумова
- Можу сформулювати, що робить тести придатними до паралелі: незалежність від порядку в коді ПЛЮС незалежність даних на бекенді.
- Розумію, чому «локально зелено, у паралелі червоно» — це майже завжди прихована залежність через спільні дані.
- Знаю тезу «паралельність не створює нових багів, вона проявляє приховані порядком» і можу її обґрунтувати.
- Розумію, що фреймворк ізолює лише браузер (свіжий
browser context), а ізоляція даних на бекенді — на моїй відповідальності.
Воркери проти шардінгу
- Знаю різницю: воркер — процес на одній машині (ліміт — ядра), шард — частина сьюти на окремій машині (ліміт — бюджет).
- Розумію, що осі перемножуються (2 машини на 4 воркери дають 8 одночасних тестів) і чому спершу воркери, потім шарди.
- Знаю, скільки воркерів реально тягне машина (близько половини ядер, 4–8) і чому «більше» починає сповільнювати.
- Розумію різницю гранулярності: між файлами паралель з коробки, всередині файлу — лише через
fullyParallelчиmode: 'parallel'. - Знаю, як зливають шарди в один звіт:
blob-репортер плюсmerge-reports.
Спільні ресурси
- Можу пояснити «один юзер — черга болю» на конкретних гонках: профіль, сесія, конфлікт унікальності, «зайве» замовлення.
- Знаю рецепт розшивання: пул акаунтів per-worker через
TEST_PARALLEL_INDEX, унікальні дані на тест, ізольований namespace/tenant. - Розумію, що нерозшивний спільний ресурс доводиться серіалізувати — і кожен серіалізований блок бʼє по прискоренню.
Балансування
- Можу пояснити, чому балансувати треба за тривалістю, а не за кількістю тестів.
- Знаю, що час прогону дорівнює часу найдовшого шарда (long pole) і що один повільний тест ставить нижню межу.
- Розумію, чому дрібна гранулярність — друг балансувальника, а файл-моноліт — «жердина».
Ціна паралелізації
- Розумію, що прискорення за годинником не зменшує сумарних машино-хвилин — це пряма ціна CI.
- Можу пояснити, чому вмикання паралелі часто піднімає флакі-рейт і що це викриття наявних проблем, а не регрес.
- Знаю закон Амдала стосовно сьюти: серіалізована частка й фіксовані накладні ставлять межу прискоренню.
Яку величину стискає паралелізація тестів?
Питання
Сумарний час (CPU) vs час за годинником (wall-clock) — що з них стискає паралель?