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): якщо частину сьюти доводиться серіалізувати (спільний ресурс, який не розшити) або одна «жердина» триває 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.