Паралелізація: воркери, шардінг, масштабування
Зміст
з тисячі 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), сформульований Джином Амдалом на конференції 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: у переліку недоліків автоматизації прямо стоять початкова інвестиція та тривала вартість — саме та рамка, у якій рахують окупність воркерів.
Відео до теми
Навіщо взагалі паралелити тести й яку саме величину це прискорює?
Паралелізація стискає час за годинником (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, Джин Амдал, конференція AFIPS 1967) обмежує прискорення задачі при додаванні ресурсів: «even with an infinite number of processors, the speedup is constrained by the unparallelizable portion». Числа дають масштаб: паралелиться половина роботи — максимум удвічі, паралелиться 95% — максимум у двадцять разів. Стосовно сюїти це означає: якщо частину тестів доводиться серіалізувати (спільний ресурс, який не розшити), то хоч скільки машин докидай — нижче цієї межі прогін не впаде. Окремо варто розвести два обмеження, які легко зліпити: жердина обмежує годинниковий час і виводиться з розкладки роботи, а не з закону Амдала; закон обмежує прискорення від додавання ресурсів. До того ж кожен воркер і шард додає фіксовані накладні на старт. Практичний висновок — на дрібній сюїті ці накладні можуть зʼїсти весь виграш: тридцятисекундний набір під паралеллю піде повільніше, ніж без неї.
Чому «мій фреймворк ізолює тести» — небезпечна ілюзія?
Бо хороший фреймворк ізолює лише браузерний рівень, а не весь стан. У 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. - Розумію, що нерозшивний спільний ресурс доводиться серіалізувати — і кожен серіалізований блок бʼє по прискоренню.
Балансування
- Можу пояснити, чому балансувати треба за тривалістю, а не за кількістю тестів.
- Знаю, що час прогону дорівнює часу найдовшого шарда і що один повільний тест ставить нижню межу; пам'ятаю, що «довга жердина» — наша робоча назва, а не галузевий термін, і що з це твердження не виводиться.
- Розумію, чому дрібна гранулярність — друг балансувальника, а файл-моноліт — «жердина».
Ціна паралелізації
- Розумію, що прискорення за годинником не зменшує сумарних машино-хвилин — це пряма ціна CI.
- Можу пояснити, чому вмикання паралелі часто піднімає -рейт і що це викриття наявних проблем, а не .
- Знаю закон Амдала стосовно сюїти: серіалізована частка й фіксовані накладні ставлять межу прискоренню.
Квіз
Перед стартом
- Питань: 13
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Сумарний час (CPU) vs час за годинником (wall-clock) — що з них стискає паралель?


