07 · Інструменти автоматизації
Playwright: фікстури, хуки і конфігурація
Зміст
Найдорожчий рядок у тестовому проєкті — не і не перевірка, а той, що готує оточення: логін, , піднятий сервіс, чистий стан. Playwright віддає цю роботу двом різним механізмам — і хукам, — а над ними тримає третій рівень, конфігурацію, де вирішується, скільки разів усе це станеться і в яких варіантах. Поки в десять тестів, різниця між механізмами не видна. На двохсотому тесті вона визначає, чи прогін вкладається в хвилини, чи розтягується на десятки, і чи падає він у паралелі.
Ця глава — про механіку саме цих трьох речей у Playwright: що тест отримує безкоштовно, як оголосити своє, чим worker відрізняється від test, і що насправді робить playwright.config.ts. Два питання — «фікстура чи beforeEach» і «як ти ганяєш ті самі тести на трьох браузерах» — миттєво показують, чи людина налаштовувала фреймворк, чи копіювала приклади. Концептуальний бік — навіщо взагалі (test runner), фікстури й хуки — канон розділу «Автоматизація»; тут — механіка конкретного інструмента.
Вбудовані фікстури: оточення, яке тест не будує
Фікстура (fixture) у Playwright — це іменований шматок оточення тесту. Джерельне означення звучить так: фікстури встановлюють оточення для кожного тесту, даючи тесту все, що йому потрібно, і нічого зайвого, і вони ізольовані між тестами. ISTQB формулює те саме ширше: фікстура — це те, що має бути доступним, аби тест міг виконатися, і вона прямо повʼязана з повторюваністю й атомарністю тесту.
Тест не запускає браузер і не створює профіль. Він просто називає в аргументах те, що йому потрібно:
import { test, expect } from '@playwright/test';
test('кошик порожній у нового користувача', async ({ page }) => {
await page.goto('/cart');
await expect(page.getByText('Кошик порожній')).toBeVisible();
});
Дужки { page } тут не магія, а замовлення: ранер читає імʼя параметра, піднімає фікстуру page і віддає її в тестову функцію. Три вбудовані фікстури варто знати назубок:
| Фікстура | Що це | Хто нею володіє |
|---|---|---|
page | ізольована сторінка цього прогону тесту | тест |
context | ізольований контекст браузера (browser context), якому належить page | тест |
browser | сам браузер, спільний між тестами заради економії ресурсів | воркер (worker) |
Уся економіка ізоляції — у цій асиметрії: браузер спільний, а контекст і сторінка — ні. «Спільний» тут означає «на воркер-процес, який виконує тести», а не «один на весь прогін»: кожен стартує власний браузер. Кожен тест дістає свіжий контекст, еквівалентний новому профілю браузера, але не платить за запуск процесу. Дока прямо називає ізоляцію причиною відтворюваності й захисту від каскадних падінь — тобто чистий стан між тестами не «прибирають», його дають новим контекстом.
Друга властивість, яку легко проґавити: фікстури ліниві. Ранер створює page і context, лише якщо тест назвав їх у своїх аргументах, і закриває їх сам. Для порівняння: у бібліотечному режимі, без ранера, ієрархію browser → context → page вибудовують вручну й закривають теж вручну — саме цю різницю розбирає глава Playwright: архітектура, браузери і контексти.
Третя властивість — не про швидкість, а про структуру: фікстури дають групувати тести за змістом, а не за спільною підготовкою. Це прямий аргумент проти файлів, зліплених за принципом «тут усе, що потребує залогіненого адміна».
test.extend і скоупи: test проти worker
Власна фікстура оголошується розширенням базового test через test.extend:
import { test as base } from '@playwright/test';
type Fixtures = { cart: Cart };
export const test = base.extend<Fixtures>({
cart: async ({ page }, use) => {
const cart = await createCart(page); // setup
await use(cart); // тут виконується тіло тесту
await cart.remove(); // teardown
},
});
Усе до use — підготовка, усе після — , яке ранер викличе сам. Фікстура може залежати від іншої (cart просить page), і порядок вибудовує ранер, а не автор.
Скоуп задається другим параметром типу і кортежним синтаксисом:
export const test = base.extend<{}, { apiToken: string }>({
apiToken: [async ({}, use) => {
const token = await issueToken();
await use(token);
}, { scope: 'worker' }],
});
Кортеж [fn, { scope: 'worker' }] — не стилістика, а єдиний спосіб сказати ранеру, що фікстуру треба поставити раз на воркер-процес. Різниця в житті двох скоупів проста: тестова фікстура прибирається після кожного тесту, воркерна — лише коли завершується сам воркер.
З цієї схеми випливають три речі.
Воркер перевикористовується не завжди. Ранер віддає одному воркеру стільки файлів, скільки може, — але тільки поки воркерні фікстури збігаються, тобто оточення ідентичне. Розвів файли різними значеннями воркерної фікстури — і замість отримав старт нового воркера з усією його ціною.
У воркерної фікстури власний , рівний дефолтному таймауту тесту. Тобто «підняти сервіс» не змагається за секунди з тілом тесту, але й безмежного часу не має. Самі рівні таймаутів розбирає глава Playwright: перевірки і автоочікування.
Скоуп — це компроміс, а не оптимізація. Воркерний скоуп економить час на дорогій підготовці, але повертає в сюїту спільний стан: усе, що тест змінює, стає видимим наступним тестам того самого воркера. Класика вибору звучить так: усе, що тести лише читають (зʼєднання, піднятий сервер, службовий токен), — кандидат на worker; усе, що тест мутує, лишається на test.
Фікстури проти хуків: різниця не в синтаксисі
Хук тесту (test hook) виконується завжди. Фікстура — лише коли її попросили. Це вся різниця, і з неї випливає решта.
Дока називає три властивості фікстур, яких у хуків немає:
- інкапсуляція — setup і teardown лежать в одному місці. Звідси готовий критерій рефакторингу: якщо
after-хук прибирає те, що створивbefore-хук, це кандидат на фікстуру; - перевикористання між файлами — оголосив раз, використовуєш у всіх тестах;
- on-demand — піднімається лише те, що тест справді просить, і нічого зайвого.
Хуки при цьому не зло. Офіційна дока прямо називає їх легітимним способом уникнути повторення й додає, що самі по собі вони ізоляції не порушують, бо жоден тест не залежить від іншого. Механіка хуків спільна для ранерів: beforeEach/afterEach — на кожен тест, beforeAll/afterAll — раз на файл; хуки верхнього рівня діють на всі тести файлу, а оголошені в describe — лише на тести свого блоку; порядок жорсткий: зовнішній beforeEach виконується раніше за вкладений. Хуки можуть бути асинхронними — власне, beforeAll/afterAll і існують для підготовки «один раз на початку файлу», яку через асинхронність не зробиш інлайн.
А тепер пастка Playwright, якої в «класичній» моделі немає. У повністю паралельному режимі «раз на файл» перестає означати «раз». Тести того самого файлу йдуть в окремих воркерах, спільного стану й глобальних змінних між ними немає, і кожен тест виконує всі релевантні хуки сам для себе — включно з beforeAll і afterAll.
Виглядає це буденно: beforeAll створює одне замовлення, три тести його читають. У паралелі створиться три замовлення, кожне у своєму воркері, — і якщо код на це не розрахований (унікальний email, номер, зайнятий слот), сюїта червона рівно там, де «економили».
| Що готуємо | Куди це кладуть |
|---|---|
| дороге зʼєднання чи сервіс, який тести лише читають | воркерна фікстура |
| дані, які тест змінює | тестова фікстура |
| повторювана дія без результату, що передається в тест | хук |
| підготовка, потрібна не всім тестам файлу | фікстура (бо on-demand) |
Діагностичний рефлекс, який радить дока Jest, вартий того, щоб довести до автоматизму: перше питання при падінні — чи падає тест, коли він єдиний у прогоні. Падає сам — проблема в тесті. Падає лише в наборі — проблема в спільному стані, тобто в хуку або в .
playwright.config: два рівні, які плутають
Конфіг ділиться на два рівні, і дока застерігає про це прямо: опції самого ранера живуть на верхньому рівні, а опції оточення тесту — в секції use, і окремою приміткою дока просить не класти перші в другу. Тобто це не косметика, а розподіл повноважень.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests', // опція ранера
fullyParallel: true, // опція ранера
forbidOnly: !!process.env.CI, // опція ранера
retries: process.env.CI ? 2 : 0, // опція ранера
use: { // оточення тесту
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
},
});
Мнемоніка проста: retries — про те, як ранер поводиться з тестом; trace і baseURL — про те, в якому оточенні тест живе. ISTQB описує той самий шов концептуально: узагальнена архітектура автоматизації відокремлює визначення тесту від системи під тестом і від інструментів, і конфіг — саме те місце, де ця межа матеріалізується.
baseURL — найдешевша інвестиція в : відносний аргумент page.goto('/cart') резолвиться відносно нього, тож середовище перемикається змінною оточення, а не правками в тестах. Резолвінг відносних адрес як явище розібраний у главі URL і кодування.
Базовий конфіг із доки навмисно розводить локальний запуск і CI: лише в CI, forbidOnly в CI (щоб забутий test.only завалив , а не тихо звузив прогін до одного тесту), на першому ретраї впалого тесту, локальний dev-сервер піднімається перед прогоном. Політику ретраїв як таку — коли вони виявляють , а коли маскують — розбирає глава Боротьба з флаком засобами інструмента.
Варто знати, що модель «конфіг = опції» не єдина. У CodeceptJS конфіг несе ще й роль реєстру : include перелічує обʼєкти, які потім інжектяться в сценарій за іменем, а хелпер задає рушій виконання (Playwright, WebDriver, Puppeteer, Appium) — тож той самий набір сценаріїв перемикається між рушіями через конфіг. Деталі — у главі CodeceptJS: актор, хелпери і сценарний стиль.
Проєкти: матриця конфігурацій замість трьох пайплайнів
Проєкт (project) у конфігурації ранера — це логічна група тестів, що виконуються з однаковою конфігурацією. Не тека, не сюїта і не «підпроєкт у таск-трекері»: той самий файл може належати кільком проєктам і прогнатися в кожному по-своєму. Приклад із самої доки — той самий тест у залогіненому й незалогіненому стані.
Браузери — лише найвідоміше застосування. Проєктами розкладають і різні таймаути, і різні ретраї, і різні середовища (staging проти прода), і поділ за пакетом чи функціональністю. Кросбраузерний прогін при цьому описується списком проєктів у конфізі, а не трьома командами запуску чи трьома .
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/, teardown: 'cleanup' },
{ name: 'cleanup', testMatch: /.*\.cleanup\.ts/ },
{
name: 'chromium',
use: { browserName: 'chromium', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { browserName: 'webkit', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
Кожен проєкт задає власну секцію use — звідси й «той самий тест, інший браузер» або «той самий тест, інший базовий URL». За замовчуванням ранер виконує всі проєкти; звузити до одного — --project, тобто рішення прогону, а не конфіга.
dependencies — це список проєктів, які мають відпрацювати перед тестами іншого проєкту. Саме так робиться «один раз перед усім»: логін, даних, прогрів кешу. Три деталі відрізняють того, хто це справді налаштовував:
- setup-тести видно в репортері, і трейс на них теж записується — підготовка дебажиться тими самими інструментами, що й тести;
- кілька залежностей виконуються паралельно, а якщо тести залежності впали, залежні тести не запускаються взагалі — вони не «червоні», бо жодного разу не виконалися;
- прибирання чіпляється до setup-проєкту окремою властивістю
teardownі відпрацьовує після всіх залежних проєктів, а не після кожного.
І пастка, яка спершу виглядає як баг ранера: будь-яка фільтрація — --grep, --shard, шлях у командному рядку, test.only — вибирає основні тести, а тести залежностей підтягуються самі. Тому шард на CI не лишається без логіну, хоч у його вибірці setup-тестів немає. Свідомо відрубати це можна прапорцем --no-deps — і тоді запустяться лише вибрані проєкти.
Механіку (storage state) розбирає глава Playwright: автентифікація і повторне використання стану, а воркери й між машинами — Паралельний запуск: workers, шардінг, браузерні ферми.
Анотації і теги: намір, а не «вимкнути тест»
Playwright підтримує теги й анотації, які показуються у звіті. Вбудованих анотацій чотири, і це чотири різні наміри — плутати їх дорого:
| Анотація | Тест запускається | Сенс |
|---|---|---|
test.skip() | ні | незастосовно в цій конфігурації |
test.fixme() | ні | падає чи крешить, запускати не варто |
test.fail() | так | мусить упасти; якщо пройде — ранер поскаржиться |
test.slow() | так | повільний: таймаут тесту втричі більший |
Пару fail проти fixme плутають, бо назви обидві звучать як «тест не працює». Насправді fail тест запускає й вимагає падіння: зелений результат стає помилкою. Це точний механізм для ситуації «баг є, і я хочу дізнатися в той день, коли його виправлять». fixme не запускає взагалі — дока радить його саме тоді, коли тест повільний або крешить.
Анотації можуть бути умовними: застосовуються, коли умова істинна, і можуть залежати від фікстур. На одному тесті їх може бути кілька, у різних конфігураціях. Тому «пропустити на WebKit» — це не if у тілі тесту:
test('вибір файлу', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'діалог не піднімається в цій збірці');
// ...
});
Теги, як і анотації, показуються у звіті, а відбір за ними робиться тим самим фільтром, що й за назвою: позначив тест — і відбираєш його через --grep; зворотний відбір, «усе, крім позначеного», — --grep-invert. Звідси й типова розкладка прогонів: @smoke на кожен PR, повна вночі.
Тут же варто розвести інструмент і теорію, бо на цьому ловлять. «Статус» у звіті ранера — не шкала ISTQB. Силабус CTFL 4.0 подає passed/failed як міру прогресу, а ширшої шкали виконання — blocked, skipped — не дає ні він, ні , який означує результат тесту просто як статус виконання тесту. Тобто test.skip — анотація конкретного ранера з власною семантикою, а не нормативний «статус кейсу»: у TMS «skipped» може означати одне, у звіті Playwright — інше, і сильна відповідь на співбесіді цю різницю називає, а не змішує.
Параметризація: рівень тесту і рівень проєкту
Дока розводить два рівні прямо: параметризувати можна на рівні тесту або на рівні проєкту. Плутанина між ними — джерело і дублювання коду, і хибних очікувань від звіту.
Рівень тесту — набір даних усередині файлу: , межі одного поля. Механіка буденна — звичайний обхід масиву, а єдина формальна вимога — унікальна назва тесту.
const cases = [
{ input: '', error: 'Порожнє поле' },
{ input: 'a@', error: 'Некоректна адреса' },
];
for (const item of cases) {
test(`валідація email: ${item.input || 'порожньо'}`, async ({ page }) => {
await page.goto('/signup');
await page.getByLabel('Email').fill(item.input);
await expect(page.getByRole('alert')).toHaveText(item.error);
});
}
Пастка тут одна, зате постійна — розміщення хуків. Дока рекомендує ставити beforeEach, beforeAll, afterEach і afterAll поза циклом, щоб вони виконалися один раз; а якщо хуки потрібні на кожен набір, їх кладуть усередину describe(), і тоді вони спрацюють на кожній ітерації. Це і є найчастіша причина скарги «у другому кейсі дані вже не ті».
Рівень проєкту — той самий тест на різних конфігураціях. Параметр оголошується як опція з дефолтом і читається в тесті так само, як фікстура:
// fixtures.ts
export const test = base.extend<{ person: string }>({
person: ['John', { option: true }],
});
// playwright.config.ts
projects: [
{ name: 'alice', use: { person: 'Alice' } },
{ name: 'bob', use: { person: 'Bob' } },
],
Перший проєкт прожене набір зі значенням Alice, другий — з Bob. Окремого data provider у Playwright немає, і це не бідність API: опція проєкту — це та сама фікстура, лише зі значенням із конфігу, і дока прямо каже, що в тесті вона читається так само, як фікстура. Комбінації конфігурацій живуть саме на цьому рівні; як їх добирати, щоб не отримати комбінаторний вибух, — тема розділу «Тест-дизайн».
Типові помилки
- Виглядає як «фікстура не спрацювала», а насправді тест її не просив. Фікстури on-demand: не назвав у аргументах — ранер її не піднімав. У хука така поведінка неможлива, тому інтуїція «підготовка ж є в файлі» тут підводить.
- Виглядає як «воркерний скоуп зекономив час», а насправді ранер підняв новий воркер. Один воркер бере наступні файли, лише поки воркерні фікстури збігаються. Різні значення воркерної фікстури = різні оточення = нові воркери з усією ціною старту.
- Виглядає як «прибирання є», а насправді teardown не знає, що встигло створитися.
after-хук, який прибирає створенеbefore-хуком, — це рознесені по файлу половинки одного механізму; дока прямо каже, що це кандидат на фікстуру, де setup і teardown лежать разом. - Виглядає як «проєкт — це тека з тестами», а насправді це конфігурація. Той самий файл може прогнатися в пʼятьох проєктах, тому фраза «упав тест логіну» без назви проєкту — не діагноз: у Chromium і WebKit це два різні результати.
- Виглядає як «залежність упала, тести червоні», а насправді вони не виконувалися. Падіння setup-проєкту не робить залежні тести
failed— воно їх не запускає. Шукати їхній результат у звіті марно, дивитись треба на setup. - Виглядає як «шард на CI побіжить без логіну», а насправді залежності підтягуються самі.
--grep,--shardіtest.onlyвибирають лише основні тести, а тести залежностей ранер додає сам; свідомо відрубати їх можна тільки прапорцем--no-deps. - Виглядає як «дані в другому кейсі не ті», а насправді хук стоїть не там. Хук поза циклом параметризації виконується один раз на всі набори — і другий набір бачить залишки першого.
Підсумок
- Фікстура дає тесту оточення на вимогу:
pageіcontextізольовані на тест, аbrowserспільний у межах воркера — саме тому ізоляція в Playwright майже безкоштовна. - Хук виконується завжди, фікстура — лише коли її попросили; звідси три властивості фікстур, яких у хуків немає: інкапсуляція setup і teardown, перевикористання між файлами, on-demand.
- Скоуп
workerекономить дорогу підготовку, але повертає спільний стан, а воркер перевикористовується лише поки воркерні фікстури збігаються. - У повністю паралельному режимі «раз на файл» не означає «раз»: кожен тест виконує
beforeAll/afterAllсам для себе, і глобальних змінних між воркерами немає. - Конфіг має два рівні (опції ранера проти секції
use), а проєкти — це матриця конфігурацій із залежностями, де setup-проєкт підтягується навіть у відфільтрований прогін.
Можливі питання
- «Що таке фікстура і чим вона краща за
beforeEach?» Сильна відповідь: setup і teardown в одному місці, перевикористання між файлами, on-demand, композиція через залежності й керований скоуп. Інтервʼюер слухає, чи звучить слово «лише коли попросили». - «Чим відрізняються скоупи
testіworker? Що покладеш у кожен?» Перевіряють розуміння ціни ізоляції: читаємо —worker, мутуємо —test. Плюс бонус: назвати умову перевикористання воркера. - «Як ти ганяєш ті самі тести на трьох браузерах?» Правильна відповідь — матриця проєктів у конфізі, а не три команди й не три пайплайни. Відповідь «три джоби в CI» одразу показує, що проєктів людина не налаштовувала.
- «Як зробити логін один раз на весь прогін?» Очікують setup-проєкт із
dependencies, а неbeforeAllу кожному файлі. Хороший кандидат додає, що падіння setup не робить тести червоними — воно їх не запускає. - «У чому різниця між
test.skip,test.fixmeіtest.fail?» Дивляться, чи розрізняєш «не запускати» і «запустити й вимагати падіння», бо це різні інструменти для різних станів . - «Тести падають у паралелі, а по одному зелені. З чого почнеш?» Місток до ізоляції: спільний стан у хуку чи воркерній фікстурі, спільний акаунт, спільні дані. Питання «а тест падає, коли він єдиний у прогоні?» тут — правильний перший крок.
Джерела
Вбудовані фікстури: оточення, яке тест не будує
- Playwright — Test fixtures — означення фікстури, ізольованість між тестами, лінивість, і те, що фікстури групують тести за змістом, а не за спільною підготовкою.
- ISTQB® Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0 — §4.2: фікстура — те, що має бути доступним, аби тест міг виконатися; фікстури дають контроль над оточенням і даними й повʼязані з повторюваністю й атомарністю.
- xUnit Test Patterns — Fresh Fixture — фікстура означена як стан тестового оточення перед тестом, а вибір «будувати її щоразу заново чи перевикористовувати» названо ключовим рішенням автоматизації.
- Playwright — Library — ранер віддає тесту вже ізольовані
pageіcontextяк вбудовані фікстури й створює їх ліниво; у бібліотеці ту саму ієрархію будують і закривають вручну. - Playwright — головна сторінка — кожен тест дістає свіжий контекст, еквівалентний новому профілю браузера, з ізоляцією майже без накладних витрат.
- Playwright — Authentication — ізоляція контекстів названа причиною відтворюваності й захисту від каскадних падінь.
- Playwright — Isolation (browser contexts) — контекст як механізм ізоляції та багатокористувацьких сценаріїв.
- Playwright — Browsers — які рушії й брендові збірки водить інструмент, тобто чим є той спільний
browser. - Playwright — Parallelism — воркер є окремим процесом ОС, який стартує власний браузер: звідси «спільний на воркер», а не «спільний на прогін».
test.extend і скоупи: test проти worker
- Playwright — Test fixtures —
test.extend, кортежний синтаксисscope: 'worker', прибирання тестової фікстури після кожного тесту й воркерної — при завершенні воркера, перевикористання воркера лише поки воркерні фікстури збігаються, окремий таймаут воркерної фікстури. - xUnit Test Patterns — Fresh Fixture — «щоразу заново проти перевикористання» як ключове рішення: це і є вибір між
testіworker. - ISTQB® Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0 — §4.2: фікстури дають контроль над оточенням і даними, а й постумови задаються явно.
Фікстури проти хуків: різниця не в синтаксисі
- Playwright — Test fixtures — три властивості, яких немає в хуків: інкапсуляція setup і teardown в одному місці, перевикористання між файлами, on-demand.
- Jest — Setup and Teardown — чотири хуки й різниця «на кожен тест проти раз на файл», скоуп
describe, порядок вкладенихbeforeEach, асинхронні хуки і діагностичне питання «чи падає тест сам». - Playwright — Best Practices — хуки названо легітимним способом уникнути повторення, і самі по собі вони ізоляції не порушують.
- Playwright — Parallelism — у повністю паралельному режимі тести не можуть ділити стан чи глобальні змінні, а всі релевантні хуки, включно з
beforeAllіafterAll, кожен тест виконує сам для себе. - ISTQB® Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0 — §4.2: явні передумови й постумови як зміст фікстури.
- xUnit Test Patterns — Fresh Fixture — свіжа фікстура проти перевикористаної як рамка для вибору «хук чи фікстура».
playwright.config: два рівні, які плутають
- Playwright — Test configuration — два рівні конфігу з прямим застереженням не класти опції ранера в
use,baseURLдля відноснихpage.goto(), і набір дефолтів доки для CI: ретраї лише в CI,forbidOnly, трейс на ретраї, підняття dev-сервера. - CodeceptJS — Configuration — інша модель конфігу: реєстр впровадження залежностей через
includeі хелпер, що задає рушій виконання. - ISTQB® Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0 — §3.1.1, §3.1.3: gTAA відокремлює визначення тесту від системи під тестом і від інструментів; шари фреймворку рекомендовано тримати низькими.
Проєкти: матриця конфігурацій замість трьох пайплайнів
- Playwright — Projects — означення проєкту, розкладка за таймаутами, ретраями й середовищами, дефолт «усі проєкти» проти
--project, залежності та їхня паралельність,teardownна setup-проєкті, і те, що фільтрація підтягує залежності, а--no-depsїх вимикає. - Playwright — Browsers — кросбраузерна матриця як список проєктів: рушії Chromium/WebKit/Firefox і брендові канали.
- Playwright — Test configuration — межа рівнів конфігу, на якій тримається власна секція
useкожного проєкту.
Анотації і теги: намір, а не «вимкнути тест»
- Playwright — Annotations and tags — теги й анотації показуються у звіті; семантика
skip/fail/fixme/slow, умовні анотації, залежні від фікстур, і відбір через--grepта--grep-invert. - ISTQB Certified Tester Foundation Level Syllabus v4.0 (копія astqb.org) — §1.4.1: виконання тестів, порівняння фактичних результатів з очікуваними й логування; §5.3.1:
passed/failedяк метрика прогресу, ширшої шкали статусів силабус не дає. - ISTQB Glossary — канонічні означення прогону й результату тесту: результат тесту означено як статус виконання тесту, самої шкали значень словник не нормує.
Параметризація: рівень тесту і рівень проєкту
- Playwright — Parameterize tests — два рівні параметризації, вимога унікальної назви тесту, розміщення хуків поза циклом проти хуків усередині
describe(), і опція проєкту (option: true), яка читається в тесті як фікстура.
- Playwright — Test fixtures — on-demand як причина «фікстура не піднялась», умова перевикористання воркера, і критерій «
afterприбирає створенеbefore» → фікстура. - Playwright — Projects — проєкт як конфігурація, а не тека; поведінка при падінні залежності; фільтрація, що тягне залежності, і
--no-deps. - Playwright — Parameterize tests — хуки поза циклом виконуються один раз, усередині
describe()— на кожну ітерацію. - Playwright — Browsers — той самий тест у різних рушіях дає різні результати, тож назва проєкту є частиною діагнозу.
- Playwright — Test configuration — два рівні конфігу: опції ранера живуть на верхньому рівні, і дока прямо застерігає не класти їх у
use. - ISTQB® Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0 — §4.2: явні передумови й постумови як умова повторюваності — те, що ламається в кожній із цих помилок.
- xUnit Test Patterns — Fresh Fixture — ціна перевикористаної фікстури, з якої й ростуть помилки скоупу.
- Playwright — Test fixtures — три властивості фікстур проти хуків, скоупи
testіworker, умова перевикористання воркера. - Playwright — Projects — матриця проєктів замість трьох пайплайнів,
dependenciesдля «логін один раз» і поведінка при падінні setup-проєкту. - Playwright — Browsers — рушії, з яких складається кросбраузерна матриця.
- Playwright — Annotations and tags — семантика
skip/fixme/fail, на якій тримається питання про різницю. - Playwright — Parallelism — у паралельному режимі тести не ділять стан і глобальні змінні, а хуки кожен тест виконує сам для себе.
- Jest — Setup and Teardown — «чи падає тест, коли він єдиний у прогоні» як перше діагностичне питання.
- xUnit Test Patterns — Fresh Fixture — «щоразу заново проти перевикористання» як рамка відповіді про скоупи.
Відео до теми
фікстури playwright і керування page object modelqa senpai | test automation
курс з playwright (fixture, hooks, network) | Як робити клінап?qa senpai | test automation
Дивимось відео "курс з playwright (fixture, hooks, network) | Як робити клінап?"HOT testing
Стрім: Що нового в Playwright 1.43? та відповіді на питання!HOT testing
ефективна організація тестів з playwrightqa senpai | test automationЩо таке фікстура в Playwright і що тест отримує без жодного налаштування?
(fixture) — іменований шматок оточення, який (test runner) готує перед тестом і прибирає після нього. Тест не стартує процес браузера й не збирає профіль: він перелічує потрібне в аргументах, а решту робить ранер. Безкоштовно доступні три: page — сторінка цього прогону тесту, context — ізольований профіль браузера, якому та сторінка належить, browser — сам процес браузера. Дві перші належать тесту й живуть рівно стільки, скільки він; третя належить (worker) і переживає всі тести, які цей воркер устигне виконати. ISTQB описує те саме поняття ширше: фікстура — усе, що мусить бути на місці, аби тест узагалі зміг виконатися, і саме через неї тест лишається повторюваним та атомарним. Практичний висновок для ревʼю: оточення, зібране руками в тілі тесту, ніхто не прибере і ніхто не перевикористає — це перший кандидат на переїзд у фікстуру.
Навіщо називати page в аргументах тесту — хіба сторінка не існує завжди?
Не існує: фікстури піднімаються на вимогу. Ранер читає імена параметрів тестової функції і створює тільки те, що там названо, — не згадав context, і окремий контекст під цей тест не зʼявиться. Запис async ({ page }) => { ... } тут не деструктуризація заради краси, а замовлення на конкретну фікстуру за іменем. Закриття теж не ваша турбота — тому в тестах під ранером немає жодного ручного close(). Наслідок, на якому спотикаються найчастіше: «фікстура не спрацювала» майже завжди означає «тест її не просив», бо у хука такої поведінки немає — він виконається незалежно від того, потрібен чи ні. Зворотний бік тієї самої властивості — економія: чистий API-тест не платить за сторінку, якої не замовляв.
Чому ізоляція тестів у Playwright майже нічого не коштує?
Бо дорогий і дешевий рівні розведені. Дорогий — процес браузера — піднімається раз на воркер і обслуговує всі його тести. Дешевий — контекст зі сторінкою — створюється щоразу заново, і за властивостями він еквівалентний свіжому профілю браузера: власні кукі, localStorage, sessionStorage. Тобто чистий стан тут не «вичищають» після тесту, його видають разом із новим контекстом, а це принципово надійніше: забути крок можна, забути створити контекст — ні. Звідси й два ефекти, які документація називає причиною такої архітектури: результат тесту відтворюваний, а падіння одного тесту не тягне за собою сусідів. Слово «спільний» у рядку про browser варто вимовляти точно: спільний у межах воркер-процесу, а не один на весь прогін, бо кожен воркер стартує собі браузер сам.
Як оголосити власну фікстуру і що означає use у її тілі?
Базовий test розширюють через test.extend, передавши обʼєкт із іменами й функціями фікстур. Усередині такої функції є єдина точка розриву — виклик use: усе, що написано вище за нього, виконається як підготовка, усе нижче — як прибирання, і викличе його ранер сам, навіть якщо тест упав. Значення, передане в use, і є те, що тест отримає в аргументі. Фікстури вміють залежати одна від одної: своя cart спокійно просить вбудований page, і порядок підйому вибудовує ранер, а не автор коду. Практичний виграш саме тут: setup і teardown лежать поруч, тож їх неможливо розсинхронізувати при рефакторингу, а сама фікстура експортується й перевикористовується в будь-якому файлі.
Чим фікстура краща за beforeEach?
Різниця не синтаксична, а в моменті виконання: хук працює завжди, фікстура — лише коли її замовили. З цього ростуть три властивості, яких у хуків немає. Перша — інкапсуляція: підготовка й прибирання живуть в одному оголошенні, а не рознесені по файлу двома половинками. Друга — : фікстуру оголошуєш раз і підключаєш у будь-якому тесті будь-якого файлу, тоді як хук привʼязаний до свого файлу чи блоку. Третя — on-demand: піднімається рівно те, що конкретний тест назвав, тож тест, якому важка підготовка не потрібна, за неї не платить. Плюс дві речі понад список: фікстури композуються через залежності, і в них є керований , чого хук не вміє в принципі. На співбесіді слухають саме формулювання «лише коли попросили» — воно показує, що людина розуміє механізм, а не переказує документацію.
Тоді хуки — антипатерн? Коли beforeEach доречніший за фікстуру?
Ні, і документація прямо називає хуки нормальним способом прибрати повторення; самі по собі ізоляцію вони не ламають, бо жоден тест від іншого не починає залежати. Проста межа така: якщо підготовка віддає в тест якийсь обʼєкт чи значення — це фікстура, бо значення треба кудись передати, а фікстура для цього й існує. Якщо це повторювана дія без результату, потрібна геть усім тестам файлу (перейти на стартову сторінку, виставити мову інтерфейсу), хук читається простіше й не вимагає окремого модуля. І один готовий критерій рефакторингу варто тримати в голові: коли afterEach прибирає рівно те, що створив beforeEach, ви вже написали фікстуру, просто розірвану навпіл.
Чим скоуп test відрізняється від worker і що покласти в кожен?
Тестова фікстура створюється й прибирається навколо кожного тесту; воркерна ставиться один раз на воркер-процес і розбирається лише тоді, коли той завершується. Синтаксично скоуп задається другим параметром типу і кортежем на кшталт [fn, { scope: 'worker' }] — це не стильова примха: іншого способу оголосити воркерний скоуп немає. Але вибір скоупу — компроміс, а не безкоштовне пришвидшення: воркерна фікстура повертає в спільний стан, і все, що один тест у ній змінив, побачать наступні тести того самого воркера. Робоче правило просте: те, що тести лише читають (підняте зʼєднання, службовий токен, запущений сервіс), — кандидат на worker; те, що тест мутує, лишається на test. Дрібниця, яку варто додати: у воркерної фікстури власний , рівний дефолтному таймауту тесту, тож довга підготовка не забирає секунди в тіла тесту, але й нескінченно чекати не може.
За якої умови ранер перевикористає воркер, а коли підніме новий?
Воркер бере наступні файли доти, доки оточення лишається тим самим, тобто доки збігаються значення воркерних фікстур. Щойно наступний файл вимагає воркерну фікстуру з іншим значенням, старий воркер завершується (з усіма своїми teardown), а замість нього стартує новий — з повною ціною запуску, включно з новим процесом браузера. Це та деталь, через яку «оптимізація на воркерному скоупі» іноді дає протилежний результат: розвели файли за різними значеннями воркерної фікстури — і замість одного довгоживучого воркера отримали серію коротких. Тому при проєктуванні воркерної фікстури варто дивитися не лише на її вартість, а й на кількість різних значень, які вона може набувати в одному прогоні.
beforeAll виконується один раз на файл. У Playwright теж?
Ні — і на цьому легко обпектися. У повністю паралельному режимі тести одного файлу розлітаються по різних воркерах, спільної памʼяті між ними немає, тому набір хуків, що стосуються тесту, кожен воркер відпрацьовує самостійно — beforeAll і afterAll включно. Тобто «раз на файл» перетворюється на «раз на воркер, який узяв тест із цього файлу». Виглядає це буденно, поки підготовка ідемпотентна, і болить миттєво, коли ні: beforeAll створює замовлення з фіксованим номером, три тести його читають — у паралелі буде три спроби створити те саме, з конфліктом на унікальному полі або зайнятим слотом. Глобальні змінні між тестами тут теж не працюють: файл виконується не в одному процесі. Якщо підготовка справді має статися один раз на весь прогін, її місце не в beforeAll, а в setup-проєкті із залежностями.
Тест зелений поодинці, але червоний у повному наборі. З чого почнеш?
З того самого питання, що й радить документація Jest: а чи падає він, коли він єдиний у прогоні. Це найдешевша розвилка з усіх: падає сам — проблема всередині тесту, і далі копаємо , дані, очікування; падає лише в наборі — проблема у спільному стані, і копати треба зовсім в іншому місці. У Playwright «інше місце» має короткий список підозрюваних: воркерна фікстура, яку хтось мутує; хук, що готує дані на кількох тестів; спільний акаунт або фіксовані значення в . Наступний крок — запустити підозрілу пару тестів разом і в різному порядку: якщо результат залежить від порядку, це вже доказ, а не гіпотеза. тут — не лікування: вони приховують симптом, а спільний стан лишається на місці й вилізе на іншому наборі.
Які два рівні має playwright.config.ts і що куди кладуть?
Верхній рівень — це поведінка самого ранера: де шукати тести, скільки воркерів, чи паралелити, скільки ретраїв, чи забороняти test.only. Секція use — це оточення, у якому виконується тест: базовий URL, браузер, , , стан сховища. Документація окремо застерігає не змішувати ці рівні, тобто не намагатися покласти опції ранера всередину use. Мнемоніка, яка рятує на співбесіді: retries описує, як ранер поводиться з тестом, а trace і baseURL описують, у якому світі тест живе. Концептуально це той самий шов, що ISTQB закладає в узагальнену архітектуру автоматизації: визначення тесту, система під тестом та інструменти розведені навмисно, і конфіг — те місце, де ця межа стає кодом.
Навіщо baseURL і що він змінює в тестах?
baseURL перетворює адресу середовища на параметр запуску. Оголосили його в use — і всі відносні переходи на кшталт page.goto('/cart') резолвляться відносно нього, тож перемикання між локальним стендом, staging і продом стає зміною змінної оточення, а не масовою правкою тестів. Це найдешевша інвестиція в сюїти, і водночас проста перевірка зрілості проєкту: якщо у файлах трапляються абсолютні адреси конкретного стенду, сюїта прибита цвяхами до одного середовища. Дрібниця, про яку варто памʼятати: працює тут звичайний резолвінг відносних URL, тому наявність чи відсутність провідного слеша в шляху змінює результат, і це джерело несподіванок при вкладених базових адресах.
Що зазвичай розводять між локальним запуском і CI у конфізі?
Чотири речі, і кожна має причину. Ретраї вмикають лише в CI: локально впалий тест хочеться бачити впалим одразу, а в друга спроба відсіює мережевий шум і дає позначку «нестабільний» замість глухого червоного. forbidOnly вмикають у CI, щоб забутий у коді test.only завалив , а не тихо звузив увесь прогін до одного тесту — це буквально захист від зеленої збірки, у якій нічого не перевірялося. Трейс типово ставлять на першу повторну спробу: писати його завжди дорого й обʼємно, а саме на ретраї він потрібен найбільше. І локальний dev-сервер піднімають перед прогоном силами самого ранера, щоб «запусти застосунок, потім тести» не було ручним кроком. Все це читається як одна ідея: конфіг знає, у якому середовищі він виконується, і поводиться відповідно.
Що таке проєкт у конфізі Playwright і чим він не є?
Проєкт (project) — це група тестів, які виконуються з однаковою конфігурацією. Він не дорівнює теці, не дорівнює сюїті й не має нічого спільного з проєктом у таск-трекері: той самий файл може входити у пʼять проєктів і прогнатися пʼять разів по-різному. Кожен проєкт має власну секцію use, тому «той самий тест у Chromium і WebKit» або «той самий тест на staging і на проді» — це два рядки конфіга, а не два набори файлів. Браузери — найвідоміше, але далеко не єдине застосування: проєктами розводять таймаути, ретраї, середовища й функціональні зрізи сюїти. Прямий наслідок для комунікації: повідомлення «упав тест логіну» без згадки, у якому саме проєкті, ще нічого не пояснює — в одному рушії там могло бути падіння, а в іншому зелено. За замовчуванням ранер виконує всі проєкти, а звуження до одного — це --project на запуску, тобто рішення прогону, а не властивість конфіга.
Як ти ганяєш ті самі тести на трьох браузерах?
Матрицею проєктів у конфізі: три записи в projects, у кожного своя секція use з потрібним browserName, а тестові файли лишаються ті самі. Одна команда запуску розкладає прогін на всі проєкти сама, звіт показує результат кожного окремо, а звузитися до одного браузера можна прапорцем --project без правок коду. Відповідь «зроблю три джоби в CI, у кожній свій виклик» на співбесіді читається однозначно: людина проєктів не налаштовувала. Різниця не косметична — три пайплайни означають три місця, де треба синхронно правити фільтри, ретраї й артефакти, і рівно нуль спільного звіту. Той самий механізм масштабується далі: додати мобільну емуляцію або другий базовий URL — це ще один запис у списку, а не ще один пайплайн.
Як зробити логін один раз на весь прогін, а не в кожному файлі?
Окремим setup-проєктом, на який інші проєкти посилаються через dependencies. Setup-проєкт відбирає свої файли за патерном (типово *.setup.ts), логіниться й зберігає стан автентифікації, а робочі проєкти вказують цей стан у своєму use і оголошують залежність — тож ранер сам гарантує порядок. Це саме той механізм, яким роблять «один раз перед усім»: логін, довідників, прогрів кешу. Практику від переказу документації тут відрізняють три подробиці: setup-тести видно у звіті й трейс на них теж пишеться, тож підготовка дебажиться тими самими інструментами, що й самі тести; кілька залежностей виконуються паралельно; прибирання вішають не на робочі проєкти, а на сам setup — окремою властивістю teardown, яка відпрацює один раз, коли завершаться всі залежні. Порівняно з beforeAll у кожному файлі виграш очевидний: у паралельному режимі хук виконався б стільки разів, скільки воркерів узяли тести.
Setup-проєкт упав. Що станеться із залежними тестами і де шукати причину?
Вони не стануть червоними — вони просто не запустяться. Ранер не виконує тести проєкту, залежність якого не пройшла, тому шукати їхній результат у звіті марно: там буде впалий setup, а результатів основної сюїти не буде, бо вона не виконувалася. Це часте джерело плутанини на чергуванні: «прогнався лише setup замість двохсот тестів» виглядає як зламаний ранер, а насправді це коректна поведінка залежностей. Розбір відповідно починається не з тестів, а з setup-проєкту — і тут якраз стає в пригоді те, що на нього пишеться трейс. Сильна відповідь на співбесіді містить саме розрізнення «не пройшли» і «не виконувалися», бо воно змінює і діагностику, і те, що ви скажете команді про стан релізу.
Ми ганяємо CI шардами й фільтруємо тести за тегом. Шард не лишиться без логіну?
Ні, і саме це спершу виглядає як магія. Фільтри працюють тільки на основних тестах: чи то --grep, чи --shard, чи шлях до файлу в команді, чи навіть забутий test.only — вибірка звужується, а тести проєктів-залежностей ранер докладає до неї сам. Тому шард, у чиїй вибірці немає жодного setup-файлу, усе одно отримає виконаний логін і збережений стан. Свідомо вимкнути це можна прапорцем --no-deps: тоді запустяться рівно вибрані проєкти, без підтягування залежностей, — інколи це потрібно, коли стан уже підготовлений зовні. Знання цієї пари деталей — хороший маркер практики: людина, яка сама розкладала прогін по шардах, памʼятає і те, що залежності тягнуться, і те, чим це відключити.
У чому різниця між test.skip, test.fixme, test.fail і test.slow — і чи це «статуси» тесту?
Це чотири різні наміри, а не чотири способи вимкнути тест. test.skip каже «у цій конфігурації перевірка не застосовна» і тест не запускає; test.fixme теж не запускає, але з іншим змістом — тест зламаний або крешить, і його не варто навіть намагатися виконати; test.fail тест запускає й вимагає падіння, тож зелений результат стає помилкою — це точний інструмент для «баг існує, і я хочу дізнатися в день, коли його полагодять»; test.slow нічого не вимикає, а лише втричі збільшує таймаут. Анотації бувають умовними й можуть спиратися на фікстури, тому «пропустити в WebKit» пишеться як test.skip(browserName === 'webkit', 'причина') всередині тесту, а не як if навколо тіла. І окремо про «статуси»: те, що ранер показує у звіті, — його власна семантика, а не нормативна шкала ISTQB. Силабус CTFL оперує парою passed/failed як метрикою прогресу, ширшої шкали виконання не задає, а глосарій означує результат тесту просто як статус його виконання — тож «skipped у Playwright» і «skipped у нашій TMS» цілком можуть означати різне, і сильна відповідь цю різницю проговорює.
Як параметризувати тести в Playwright і чому тут немає окремого data provider?
Рівнів параметризації два, і плутати їх дорого. Рівень тесту — це варіанти даних у межах одного файлу: кілька , значення на межах поля, набір різних вводів. Механіка буденна — звичайний цикл по масиву, що породжує окремі виклики test(), а єдина формальна вимога — унікальна назва кожного тесту, інакше їх не розрізнити ні у звіті, ні у фільтрах. Тут же чатує найчастіша пастка: хуки треба ставити поза циклом, якщо вони мають відпрацювати один раз, а якщо потрібні на кожен набір — загортати ітерацію в describe(), і тоді хук спрацює на кожній. Саме неправильне розміщення хука породжує класичну скаргу «на другому наборі дані вже не ті, хоча код той самий». Рівень проєкту — це той самий тест у різних конфігураціях: параметр оголошується як опція з дефолтом (option: true), кожен проєкт задає своє значення у use, а в тесті воно читається так само, як звичайна фікстура. Тому окремий data provider тут і не потрібен: опція проєкту — це фікстура, значення якої приходить із конфіга.
Три ситуації, у яких різниця між , хуком і конфігом перестає бути теорією: рефакторинг пари beforeEach/afterEach у фікстуру, розбір , що зеленіє поодинці й червоніє в паралелі, і розкладка прогону проєктами замість трьох . Скрізь — що дивитися і чому саме так.
Кейс 1. Пара хуків, яку варто зробити фікстурою
У репозиторії лежить типовий файл: замовлення створюється перед кожним тестом і видаляється після. Виглядає акуратно, поки не почнеш переносити ці тести кудись іще.
import { test, expect } from '@playwright/test';
test.describe('історія замовлень', () => {
let orderId: string;
test.beforeEach(async ({ request }) => {
orderId = await createOrder(request);
});
test.afterEach(async ({ request }) => {
await deleteOrder(request, orderId); // прибирає те, що створив хук вище
});
test('замовлення зʼявляється в історії', async ({ page }) => {
await page.goto(`/orders/${orderId}`);
await expect(page.getByTestId('order-status')).toHaveText('Оплачено');
});
test('замовлення можна скасувати', async ({ page }) => {
await page.goto(`/orders/${orderId}`);
await page.getByRole('button', { name: 'Скасувати' }).click();
await expect(page.getByTestId('order-status')).toHaveText('Скасовано');
});
});
Той самий код у вигляді фікстури — з окремого модуля, який імпортують інші файли:
// fixtures.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{ orderId: string }>({
orderId: async ({ request }, use) => {
const id = await createOrder(request); // setup
await use(id); // тіло тесту бачить id в аргументі
await deleteOrder(request, id); // teardown поруч із setup
},
});
export { expect } from '@playwright/test';
// orders.spec.ts
import { test, expect } from './fixtures';
test('замовлення зʼявляється в історії', async ({ page, orderId }) => {
await page.goto(`/orders/${orderId}`);
await expect(page.getByTestId('order-status')).toHaveText('Оплачено');
});
test('пошук не потребує замовлення', async ({ page }) => {
await page.goto('/orders'); // orderId не названо — його й не створять
await expect(page.getByPlaceholder('Пошук')).toBeVisible();
});
Що дивитися і чому:
- Змінна
let orderIdміж хуками — це і є симптом. Вона існує лише тому, що результат підготовки нікуди передати: хук нічого не повертає в тест. У фікстурі значення віддається черезuse, тож проміжна змінна зникає разом із , що хтось її перевизначить. afterEach, який прибирає створенеbeforeEach, — половинки одного механізму. Достатньо комусь додати раннійreturnчи третій хук — і перестане відповідати підготовці. У фікстурі їх фізично не можна рознести: рядок нижче заuseвиконає сам, навіть якщо тест упав.- Другий тест у новій версії за підготовку не платить. Він не назвав
orderId, тож жодного замовлення для нього не створюється — а в старій версіїbeforeEachвідпрацював би все одно. На сюїті з двохсот тестів, де половині дані не потрібні, це різниця в хвилинах прогону. - Фікстура переїжджає між файлами, хук — ні. Хук привʼязаний до свого
describeчи файлу, тому при переносі тестів його копіюють — і з цього моменту існують дві копії, що розходяться. Імпорт ізfixtures.tsцю проблему знімає за побудовою. - Групувати тести тепер можна за змістом. Зникає потреба тримати в одному файлі все, що потребує однакової підготовки: підготовка приходить із фікстурою, а файли ріжуться за функціональністю.
Кейс 2. «У паралелі червоно, поодинці зелено»: beforeAll і три воркери
Тікет: сюїта оплати падає в CI приблизно на третині прогонів, локально з --workers=1 завжди зелена. У файлі — економія на підготовці.
test.describe.configure({ mode: 'parallel' });
let orderNumber: string;
test.beforeAll(async ({ request }) => {
// фіксований номер, бо «замовлення ж одне на файл»
orderNumber = await createOrder(request, { number: 'ORD-1001' });
});
test('чек містить номер замовлення', async ({ page }) => { /* ... */ });
test('лист про оплату відправлено', async ({ page }) => { /* ... */ });
test('замовлення видно адміну', async ({ page }) => { /* ... */ });
У повністю паралельному режимі три тести можуть розлетітися по трьох — і beforeAll виконається тричі, по разу в кожному процесі. Далі варіанти: другий і третій виклики падають на унікальному номері, або створюють по замовленню, і тест адміна бачить у списку три записи замість одного.
Порядок гіпотез, за яким розбираю такі падіння:
| Що спостерігаю | Що це означає | Куди дивитися далі |
|---|---|---|
| поодинці зелено, у наборі червоно | спільний стан, а не логіка тесту | хуки файлу і скоуп фікстур, а не локатори |
| падає лише під паралеллю, з одним воркером зелено | «раз на файл» перетворилося на «раз на воркер» | beforeAll/afterAll і все, що вони створюють з фіксованими значеннями |
| помилка на унікальному полі (email, номер, слот) | підготовка виконалася більше разів, ніж очікував автор | зробити дані унікальними або перенести підготовку в setup-проєкт |
| впав тест, який нічого не створює | побічний ефект чужої підготовки | чи не мутує хтось воркерну фікстуру, яку решта лише читає |
Виправлення залежить від того, чого підготовка мала досягти:
// А. Дані потрібні кожному тесту свої — тестова фікстура з унікальним номером
export const test = base.extend<{ order: Order }>({
order: async ({ request }, use) => {
const order = await createOrder(request, { number: `ORD-${Date.now()}-${process.pid}` });
await use(order);
await deleteOrder(request, order.id);
},
});
// Б. Підготовка справді одна на прогін — це setup-проєкт, а не beforeAll
// (див. кейс 3: projects + dependencies)
Що дивитися і чому:
--workers=1— не фікс, а інструмент діагностики. Він підтверджує гіпотезу «справа у паралелі» за один прогін, але лишати його в CI означає віддати найдорожчу властивість ранера заради того, щоб не чіпати підготовку.- Глобальні змінні між тестами не працюють у принципі. Файл не виконується в одному процесі, тому
letна верхньому рівні — це «по копії на кожен воркер», а не спільна памʼять. Тести, які передають стан такою змінною, під паралеллю зламані навіть тоді, коли випадково зеленіють. - тут маскує, а не лікує. Друга спроба піде в іншому воркері й може випадково пройти — рівно доти, доки склад шарда не зміниться. Фікс — прибрати спільний стан, а не збільшити кількість спроб.
- Унікальність даних дешевша за координацію. Штамп часу чи ідентифікатор процесу в номері замовлення коштує один рядок; спроба навчити три воркери домовлятися про один запис коштує архітектуру.
Кейс 3. Конфіг і проєкти: логін один раз, три браузери, один звіт
Задача типова: тести мають ганятися в Chromium і WebKit, логін виконуватися один раз, а прибиратися в кінці. Найгірше рішення — три команди запуску й beforeAll із логіном у кожному файлі. Штатне — проєкти із залежностями.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests', // рівень ранера
fullyParallel: true, // рівень ранера
forbidOnly: !!process.env.CI, // рівень ранера
retries: process.env.CI ? 2 : 0, // рівень ранера
use: { // оточення тесту
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/, teardown: 'cleanup' },
{ name: 'cleanup', testMatch: /.*\.cleanup\.ts/ },
{
name: 'chromium',
use: { browserName: 'chromium', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { browserName: 'webkit', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
});
Куди що кладеться — коротка :
| Опція | Рівень | Чому саме там |
|---|---|---|
retries, workers, fullyParallel, forbidOnly | верхній | описують поведінку ранера щодо тестів |
baseURL, trace, viewport, locale | секція use | описують оточення, у якому тест виконується |
browserName, storageState | use конкретного проєкту | те, чим проєкти й відрізняються один від одного |
| вибір, які проєкти зараз ганяти | командний рядок (--project) | це рішення прогону, а не властивість конфіга |
Типові команди, які варто вміти пояснити:
npx playwright test # усі проєкти: setup → chromium і webkit → cleanup
npx playwright test --project=chromium # лише chromium, але setup усе одно відпрацює
npx playwright test --grep @smoke # фільтр по тегу; тести залежностей підтягнуться самі
npx playwright test --shard=2/4 # шард із чотирьох; логін у ньому буде
npx playwright test --project=chromium --no-deps # свідомо без setup: стан підготовлено зовні
Що дивитися і чому:
- Фільтр не відрубує підготовку.
--grep,--shard, шлях у командному рядку і навітьtest.onlyвибирають лише основні тести, а проєкти-залежності ранер додає сам. Саме тому шард номер два не лишається без логіну, хоч жодного setup-файлу в його вибірці немає. - Червоний setup — це не червоні тести. Якщо логін упав, залежні проєкти не запустяться взагалі: у звіті буде впалий setup, а основна сюїта лишиться без результатів, бо не виконувалася. «Прогнався лише setup замість двохсот тестів» тут не збій ранера, а коректна поведінка залежностей — і розбір починається з setup, на який теж пишеться .
teardownвішається на setup-проєкт, а не на робочі. Прибирання спрацює один раз після всіх залежних проєктів, тож дані, потрібні і Chromium, і WebKit, не зникнуть посеред прогону.- Назва проєкту — частина діагнозу. «Упав тест оплати» без згадки проєкту нічого не означає: у Chromium і WebKit це два різні результати, і рівно з цієї різниці починається розмова про кросбраузерний дефект.
- Той самий механізм дає параметризацію рівня проєкту. Якщо оголосити опцію (
person: ['John', { option: true }]) у своїх фікстурах, кожен проєкт задає їй власне значення черезuse, а тест читає її як звичайну фікстуру — це і є заміна data provider, якого в Playwright немає окремою сутністю.
Вбудовані фікстури й ізоляція
- Можу без підказки сказати, кому належить кожна вбудована :
pageіcontext— тесту,browser— (worker); «спільний браузер» означає «спільний у межах воркер-процесу», бо кожен воркер стартує собі власний. - Розумію, чому ізоляція тут майже безкоштовна і чому чистий стан не «прибирають», а видають: дорогий процес піднімається рідко, а свіжий контекст із власними кукі й сховищем створюється на кожен тест.
- Знаю, що фікстури піднімаються на вимогу: не назвав у аргументах — (test runner) її не створював; закривати за собою теж не треба, це робить ранер.
test.extend і скоупи
- Можу пояснити анатомію
test.extend: рядки вище за викликuseготують оточення, рядки нижче його розбирають, аргументuseприходить у тест, а порядок залежних фікстур вибудовує ранер, а не автор. - Памʼятаю єдиний спосіб задати воркерний — кортеж
[fn, { scope: 'worker' }]у другому параметрі типу; іншого синтаксису тут немає. - Можу назвати умову воркера: він бере наступні файли, лише поки значення воркерних фікстур збігаються, інакше стартує новий воркер із повною ціною запуску.
- Розумію, що скоуп — компроміс, а не оптимізація:
workerекономить дорогу підготовку, але повертає спільний стан (читаємо —worker, мутуємо —test), а власний воркерної фікстури дорівнює дефолтному таймауту тесту.
Фікстури проти хуків
- Формулюю різницю одним реченням — хук спрацьовує в будь-якому разі, а фікстура лише тоді, коли її замовили — і називаю три властивості фікстур: підготовка й в одному оголошенні, імпорт у будь-який файл, підйом на вимогу.
- Маю готовий критерій рефакторингу: якщо
afterEachприбирає рівно те, що створивbeforeEach, це вже фікстура, просто розірвана навпіл; при цьому хук не антипатерн — він доречний там, де дія потрібна всім тестам файлу й не віддає в тест жодного значення. - Знаю пастку паралельного режиму: тести одного файлу йдуть у різних воркерах, тому
beforeAllіafterAllкожен тест виконує сам для себе, а глобальні змінні між ними не працюють. - Починаю розбір нестабільного тесту з питання «чи падає він, коли він єдиний у прогоні»: падає сам — проблема в тесті, падає лише в наборі — у спільному стані (хук або ).
playwright.config: два рівні
- Не плутаю рівні конфіга:
testDir,fullyParallel,retries,forbidOnly— опції ранера на верхньому рівні, аbaseURL,trace,browserName,storageState— оточення тесту в секціїuse. - Можу пояснити
baseURLчерез наслідок: адреса стенду стає параметром запуску, тож відносні переходи працюють усюди однаково, а перехід зі staging на прод не вимагає жодної правки в тестових файлах. - Знаю типову розкладку «локально проти CI»: тільки в ,
forbidOnlyтеж (щоб залишенийtest.onlyламав збірку, а не мовчки скорочував прогін), на першій повторній спробі, dev-сервер піднімає сам ранер.
Проєкти й залежності
- Розумію, що проєкт (project) — це конфігурація, а не тека: один файл може прогнатися в кількох проєктах, тому «упав тест логіну» без назви проєкту не є діагнозом; за замовчуванням ідуть усі проєкти, а
--project— рішення прогону, не конфіга. - Можу описати механіку
dependencies: setup-проєкт відпрацьовує перед залежними, кілька залежностей ідуть паралельно,teardownчіпляється до setup-проєкту й спрацьовує після всіх залежних, а падіння setup не робить залежні тести червоними — воно їх не запускає, тож шукати їхній результат у звіті марно. - Знаю, що
--grep,--shard, шлях у командному рядку йtest.onlyфільтрують лише основні тести, а залежності ранер підтягує сам; вимкнути це можна тільки прапорцем--no-deps.
Анотації, теги, параметризація
- Розрізняю чотири анотації за наміром:
skip— не застосовно,fixme— зламано й запускати не варто,fail— запускається й мусить упасти (зелений результат стає помилкою),slow— потрійний таймаут; умовний пропуск пишу анотацієюtest.skip(browserName === 'webkit', 'причина'), а неifнавколо тіла. - Знаю, що теги відбираються тим самим фільтром, що й назви (
--grep, зворотно--grep-invert), і не видаюskippedу звіті ранера за нормативний статус ISTQB: силабус дає лише passed/failed як метрику прогресу. - Розрізняю два рівні параметризації — цикл по масиву даних усередині файлу (з обовʼязково унікальними назвами тестів) і опцію проєкту (
option: true), яку тест читає як фікстуру, — і памʼятаю правило хуків: поза циклом вони спрацюють один раз, усерединіdescribe()— на кожній ітерації.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Фікстура (fixture) у Playwright — що це і як тест її отримує?