QA Interview Hub · База знань · Тести
Playwright
Фікстури й структура тестів
Фікстури і скоупи
Фікстури в Playwright — механізм dependency injection: тест оголошує, що йому потрібно, наприклад { page, request }, і раннер це готує. Кастомні фікстури створюють через test.extend: код до await use(value) — це setup, після — teardown. Фікстури можуть залежати одна від одної і створюються лише тоді, коли тест їх запросив.
Скоупів два: test (за замовчуванням) — фікстура створюється для кожного тесту окремо, і worker — один раз на воркер для всіх його тестів. Скоуп задається третім параметром: [async ({}, use) => {...}, { scope: 'worker' }]. Worker-скоуп підходить для дорогих речей, як підключення до БД чи тестовий користувач.
Воркерів кілька, тож worker-фікстура створиться в кожному з них — для унікальних даних використовують workerInfo.workerIndex. Опція auto: true запускає фікстуру без явного запиту, а option: true робить її параметром, що задається в конфізі. Типовий патерн — фікстури з готовими Page Objects, щоб тест одразу отримував loginPage.
Хуки, describe і умовні тести
Структура тестів: test.describe групує тести, хуки beforeEach, afterEach, beforeAll, afterAll виконують підготовку й прибирання. beforeEach має доступ до тих самих фікстур, що й тест, наприклад page. beforeAll — лише до worker-фікстур, бо виконується один раз на воркер, а не на тест.
Коли обирати хук, а коли фікстуру? Хук простіший для одного файлу. Фікстура краща, коли логіка повторюється у багатьох файлах, коли потрібен гарантований teardown і коли важливо, щоб підготовка запускалась лише для тестів, яким вона потрібна.
Керування виконанням:
test.skip(condition, 'причина') — пропустити за умовою, наприклад для браузера.test.fixme() — тест відомо зламаний, не запускати.test.fail() — тест має впасти; якщо пройде, це помилка. Корисно для відомого бага: щойно його виправлять, тест про це повідомить.test.slow() — потроїти таймаут.
test('експорт PDF', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'Баг PAY-512 у WebKit');
await page.goto('/reports');
});
test('швидкий пошук', { tag: ['@smoke', '@search'] }, async ({ page }) => {
await page.goto('/search');
});
Теги задаються в деталях тесту і фільтруються через --grep @smoke або --grep-invert @slow. Анотації annotation: { type: 'issue', description: 'PAY-512' } видно в HTML-звіті.
На співбесіді: Хуки я використовую для простої підготовки в одному файлі, а повторювану логіку з teardown виношу у фікстури. Для відомих багів беру test.fail з посиланням на тікет, щоб тест сам повідомив, коли баг виправлять. Смоук-набір позначаю тегами й запускаю через --grep.
test.step і режими describe.configure
test.step ділить тест на іменовані кроки. У звіті й trace видно не плаский список кліків, а «Логін», «Додати товар», «Оплатити», і одразу зрозуміло, на якому бізнес-кроці впало. Крок може повертати значення:
test('покупка', async ({ page }) => {
const orderId = await test.step('Оформити замовлення', async () => {
await page.getByRole('button', { name: 'Купити' }).click();
return page.getByTestId('order-id').innerText();
});
await test.step('Перевірити статус', async () => {
await expect(page.getByText(`Замовлення ${orderId}`)).toBeVisible();
});
});
Кроки зручно огортати в методи Page Object. Опція { box: true } робить так, що помилка вказує на виклик кроку в тесті, а не на рядок усередині хелпера, що спрощує читання звіту.
test.describe.configure задає режим виконання файлу чи блоку:
| Режим | Поведінка |
|---|
| default | тести файлу послідовно в одному воркері, але падіння одного не скасовує інші |
| parallel | тести блоку розподіляються між воркерами |
| serial | послідовно; при падінні решта пропускається, а при ретраї перезапускається вся група |
Також через configure можна задати retries і timeout для групи. Serial — виняток для довгих залежних сценаріїв; майже завжди краще зробити тести незалежними через підготовку даних по API.
На співбесіді: Я огортаю бізнес-етапи в test.step, щоб звіт читався як сценарій. Режим serial використовую рідко, бо при ретраї він перезапускає всю групу; натомість роблю тести незалежними. Parallel вмикаю для файлів, де тести не ділять дані.
Фікстури глибше: worker, auto, option, mergeTests
Worker-фікстура з унікальними даними для кожного воркера:
type WorkerFixtures = { account: { email: string; password: string } };
export const test = base.extend<{}, WorkerFixtures>({
account: [async ({ playwright }, use, workerInfo) => {
const api = await playwright.request.newContext({ baseURL: process.env.API_URL });
const email = `qa+w${workerInfo.workerIndex}-${Date.now()}@test.com`;
await api.post('/api/test/users', { data: { email, password: 'Secret123' } });
await use({ email, password: 'Secret123' });
await api.delete(`/api/test/users/${email}`);
await api.dispose();
}, { scope: 'worker' }],
});
Увага: вбудовані фікстури request і page мають test-скоуп, тому worker-фікстура не може від них залежати — Playwright кине помилку. Тут використовується worker-фікстура playwright, з якої створюється власний API-контекст, а в кінці він закривається через dispose().
Auto-фікстура запускається для кожного тесту без явного запиту: [async ({ page }, use) => {...}, { auto: true }]. Підходить для збору логів, перевірки JS-помилок, прикріплення артефактів.
Option-фікстура — параметр зі значенням за замовчуванням, який можна перевизначити в проєкті конфігу чи через test.use:
export const test = base.extend<{ currency: string }>({
currency: ['UAH', { option: true }],
});
Вбудовані фікстури можна перевизначати: наприклад, page, що одразу відкриває baseURL.
Коли фікстур багато, їх розносять по модулях і об'єднують через mergeTests(dbTest, apiTest, uiTest), а кастомні матчери — через mergeExpects. Фікстурі також можна задати окремий timeout, щоб повільна підготовка не з'їдала час тесту.
На співбесіді: Для дорогої підготовки я роблю worker-фікстуру з workerIndex, щоб воркери не конфліктували за дані. Auto-фікстури використовую для крос-тестових перевірок, option-фікстури — для параметрів, що змінюються між проєктами. Великий набір фікстур розбиваю на модулі й збираю через mergeTests.