Тестовий стек зсередини: ранер, фікстури, хуки, конфігурація
Зміст
Junior приходить в автоматизацію і бачить магію: пишеш test('...'), запускаєш одну команду — і звідкись беруться браузер, паралельні , при падінні й HTML-звіт. Поки все зелене, магія не заважає. Але щойно тест починає падати «через раз», або beforeEach виконується двічі, або дані з одного тесту раптом впливають на інший — з'ясовується, що ти не розумієш, хто саме і в якому порядку все це запускає. І години йдуть на дебаг інфраструктури замість продукту.
Ця глава — карта тестового стека зсередини: що таке (test runner), звідки беруться перевірки, як влаштовані хуки й , де живе конфігурація і як твій код перетворюється на звіт. Питання «чим beforeAll відрізняється від beforeEach» чи «що таке фікстура» миттєво відділяє того, хто розуміє свій інструмент, від того, хто копіює приклади не думаючи.
Тестовий стек — це не один інструмент, а кілька шарів
Слово «Playwright» чи «CodeceptJS» звучить як єдина сутність, але за ним ховається кілька окремих ролей. Розуміти їх окремо важливо, бо на різних проєктах вони зібрані по-різному: десь усе в одному пакеті, десь — з окремих бібліотек, склеєних вручну.
| Шар | Роль | Приклади |
|---|---|---|
| Ранер (test runner) | Знаходить тести, оркеструє їх, ізолює, збирає вердикти | Playwright Test, Jest, Vitest, Mocha |
| Бібліотека асертів (assertion library) | Формулює перевірку і кидає помилку при розбіжності | Playwright expect, Jest expect, Chai |
| Драйвер / адаптер | Керує браузером чи транспортом | Playwright, WebDriver, CodeceptJS-хелпери |
| Фікстури й хуки | Готують і прибирають оточення тесту | вбудовані в ранер |
| Конфігурація | Виносить параметри (оточення, таймаути) з коду | playwright.config.ts, codecept.conf.ts |
| Репортер (reporter) | Перетворює вердикти на звіт для людини й CI | HTML, list, JUnit XML, Allure |
Playwright і Cypress постачають усі шари одним пакетом. Класична зв'язка Mocha + Chai + WebdriverIO, навпаки, збирається з окремих деталей: Mocha — ранер, Chai — , WebdriverIO — драйвер. CodeceptJS додає над цим сценарний шар (I.click, I.see), а всередині історично спирається на Mocha. Коли на співбесіді питають «чим Playwright відрізняється від Selenium», половина відповіді — саме про те, які шари вони беруть на себе, а які лишають тобі.
Ранер: оркестратор, який виносить вердикт
Ранер — це диригент. Сам він нічого не «тестує»: він знаходить твої файли (зазвичай за маскою на кшталт *.spec.ts), будує з describe і test дерево тестів, розкладає їх по воркерах, запускає в правильному порядку хуки й фікстури, ізолює кожен тест і збирає результати, щоб віддати репортеру.
describe (або test.describe) групує тести; test (в інших ранерах — it) описує один кейс. Тіло тесту — звичайна .
import { test, expect } from '@playwright/test';
test.describe('Логін', () => {
test('успішний вхід', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByRole('button', { name: 'Увійти' }).click();
await expect(page.getByRole('heading')).toHaveText('Кабінет');
});
});
Ключова думка, яку junior часто пропускає: ранер вважає тест проваленим тоді, коли з його функції вилітає виняток (exception). Немає ніякого окремого «статусу fail», який тест сам собі проставляє. Дійшли до кінця функції без винятку — passed. Десь усередині щось кинуло помилку — failed. А хто найчастіше кидає ту помилку? Бібліотека асертів.
Бібліотека асертів: перевірка, що вміє падати
expect — це фасад над набором матчерів (matchers): toBe, toEqual, toHaveText, toBeVisible тощо. Механізм простий: матчер порівнює фактичне значення з очікуваним і, якщо вони не збігаються, кидає AssertionError. Ранер ловить її й позначає тест червоним. Ось і весь зв'язок між «перевіркою» в голові тестувальника та «вердиктом» у звіті.
Бібліотеки асертів відрізняються не лише синтаксисом. Найважливіша для AQA відмінність — web-first assertions у Playwright. Звичайний expect(2).toBe(2) перевіряє значення один раз, тут і зараз. А expect(locator).toBeVisible() не перевіряє одноразово: він повторює перевірку, поки умова не стане істинною або поки не вичерпається очікування (expect timeout). Це вбудований захист від гонок: елемент, який з'явиться за 200 мілісекунд, не завалить тест.
Звідси одна з найпоширеніших пасток новачка:
// ПОГАНО: значення «вирвано» ДО перевірки, ретраю немає
expect(await locator.isVisible()).toBe(true);
// ДОБРЕ: web-first assertion сама чекає й повторює
await expect(locator).toBeVisible();
У першому рядку await виконує isVisible() один раз, отримує false, і toBe(true) одразу падає — ти власноруч викинув . Це типове джерело (детальніше — у главі /avtomatyzatsiia/flaki-testy-prychyny-diahnostyka-likuvannia).
Ще одна практична річ — hard vs soft перевірки. Звичайний expect при першій розбіжності зупиняє тест (hard). expect.soft фіксує провал, але дає тесту дійти до кінця, щоб зібрати всі помилки за один прогін — зручно, коли перевіряєш багато незалежних полів на одній сторінці. Різницю між assertion як перевіркою й архітектурою тесту докладніше розбирає глава /avtomatyzatsiia/anatomiia-avtotestu-aaa-nezalezhnist-atomarnist.
Хуки життєвого циклу і їхні пастки
Хуки (lifecycle hooks) — це місця, куди ти вішаєш підготовку і . Їх чотири, і різниця між ними — питання «як часто»:
beforeAll— раз перед усіма тестами (у Playwright — раз на файл у межах воркера, що його виконує);beforeEach— перед кожним тестом;afterEach— після кожного тесту, навіть якщо той упав;afterAll— раз наприкінці.
Те, що afterEach виконується навіть після провального тесту, — не дрібниця, а основа прибирання: саме тому видалення даних вішають туди, а не в кінець тіла тесту (до кінця тіла впалий тест просто не доходить).
Головні пастки:
Спільний мутабельний стан у beforeAll. Спокуса створити користувача чи відкрити з'єднання один раз і користуватися ним у всіх тестах економить секунди, але вбиває незалежність. Якщо тест №2 змінив цей стан, тест №3 отримає його зіпсованим — і тести починають залежати від порядку виконання. Варто ввімкнути рандомізацію чи паралель — і розсипається. Стан, що змінюється, має жити на рівні beforeEach або фікстури, а не beforeAll.
Помилка в beforeAll валить усі тести файлу. Якщо підготовка на рівні файлу впала, ранер не має середовища для жодного тесту — усі вони позначаються як провалені або пропущені. Один крихкий крок у beforeAll перетворюється на десяток червоних тестів, серед яких справжню причину видно не одразу.
Розсинхрон setup і teardown. Логіка «підготувати в beforeEach, прибрати в afterEach» здається симетричною, але вона крихка: якщо beforeEach упав на півдорозі (створив половину даних і кинув виняток), afterEach уже не знає, що саме встигло створитися й що прибирати. Setup і teardown лежать у різних місцях і нічого не знають одне про одного. Саме цю проблему елегантно розв'язують фікстури.
Фікстури: setup і teardown в одному місці, на вимогу
Фікстура (fixture) — це іменований шматок оточення тесту, у якому підготовка і прибирання зшиті докупи й видаються тесту на вимогу. Тест просто оголошує, що йому потрібен authedPage, і отримує вже залогінену сторінку; звідки вона взялась і хто її потім прибере — не його турбота.
import { test as base, type Page } from '@playwright/test';
type Fixtures = { authedPage: Page };
export const test = base.extend<Fixtures>({
authedPage: async ({ page }, use) => {
await page.goto('/login'); // setup
await login(page);
await use(page); // віддати підготовлену сторінку тесту
await page.context().clearCookies(); // teardown: код ПІСЛЯ use
},
});
Усе, що до use, — підготовка; усе, що після, — прибирання, яке ранер викличе автоматично. Чим це краще за beforeEach:
- Setup і teardown поруч. Немає розсинхрону: прибирання бачить те саме, що створила підготовка.
- На вимогу (lazy). Фікстура запускається, тільки якщо тест її попросив у параметрах.
beforeEachвиконується перед кожним тестом безвідмовно, навіть коли конкретному тесту його результат не потрібен. - Композиція. Фікстури можуть залежати одна від одної (
authedPageспирається наpage), і ранер сам вибудовує правильний порядок — ми читаємо це як (dependency injection) за іменем параметра. Сама дока Playwright цього терміна не вживає (прямо його вживає CodeceptJS), тож це наша інтерпретація механізму.
Окрема важлива вісь — скоуп (scope). Фікстура за замовчуванням test-скоупу: створюється і прибирається на кожен тест — це максимальна ізоляція. Але дорогі ресурси (піднятий сервер, з'єднання з БД) розумно робити worker-скоупними: тоді вони створюються раз на воркер і перевикористовуються всіма тестами в ньому.
export const test = base.extend<{}, { server: Server }>({
server: [async ({}, use) => {
const server = await startServer();
await use(server);
await server.close();
}, { scope: 'worker' }],
});
Тут ховається компроміс: worker-скоуп економить час, але повертає спільного стану — усе, що написано в попередньому розділі про beforeAll, стосується і worker-фікстур. Зв'язок фікстур зі скоупом воркера докладніше розбирає глава /avtomatyzatsiia/paralelizatsiia-vorkery-shardinh-masshtabuvannia, а стратегію самих даних — /avtomatyzatsiia/test-dani-fabryky-fikstury-izoliatsiia (це канонічна глава про фабрики й ізоляцію). У CodeceptJS роль фікстур частково грають хелпери й Before/After, а спільні дані прокидають через — модель інша, але ідея «підготуй оточення й ізолюй тест» та сама.
Конфігурація: оточення, base URL, таймаути
Конфіг — це файл, куди виносять усе, що не є логікою конкретного тесту: на якому середовищі бігти, звідки брати адресу, скільки чекати. Прибите цвяхами в коді значення (page.goto('https://staging.example.com/login')) означає, що той самий тест не запустиш ні локально, ні на проді без правок. Тому адресу задають один раз через baseURL, а в тестах пишуть відносні шляхи.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
},
timeout: 30_000, // на весь тест
expect: { timeout: 5_000 }, // на одну web-first перевірку
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
});
Тепер await page.goto('/login') у тесті сам підставить baseURL, а середовище перемикається однією змінною оточення (BASE_URL), яку в CI підкладають зі сховища секретів (механіка секретів — тема розділу «Git і CI/CD»). Дефолт через ?? рятує локальний запуск.
Найбільша плутанина новачка — таймаути бувають різних рівнів, і це різні речі:
| Таймаут | Що обмежує |
|---|---|
| test timeout | Скільки живе весь тест цілком |
| expect timeout | Скільки одна web-first перевірка повторює спробу |
| action timeout | Скільки чекає одна дія (click, fill) |
| navigation timeout | Скільки чекає перехід на сторінку |
Перші два — це timeout і expect.timeout із прикладу вище, тобто те, що описує сторінка конфігурації Playwright. Таймаути дії та навігації задають окремими опціями, і їхні точні імена й дефолти дивись у доці свого інструмента: тут вони наведені як рівні, а не як рецепт конфігу.
Плутати рівні — значить ставити хибний діагноз. «Тест падає за 5 секунд, хоча я дав йому 30» — класика: 30 секунд це timeout на весь тест, а конкретна перевірка впала по своєму 5-секундному expect timeout. Поле projects — окремий важіль: воно множить один і той самий набір тестів на матрицю браузерів чи середовищ (той самий сценарій на Chromium і WebKit — два «проєкти» з різним use).
Теги й фільтрація: політика, а не декор
Коли тестів стають сотні, ганяти всі на кожен пуш стає дорого. Тут вмикається фільтрація. Тег (у Playwright — рядок на кшталт @smoke у назві тесту або в опції tag) — це мітка, за якою ранеру кажуть, що саме запускати.
test('швидка перевірка входу', { tag: '@smoke' }, async ({ page }) => {
// ...
});
Запуск лише позначених:
npx playwright test --grep @smoke
Фільтрувати можна не тільки за тегом: за файлом, за фрагментом назви, за проєктом. Практична цінність — розкладка прогонів: швидкий @smoke ганяють на кожен PR, а повну — вночі за розкладом. Це не косметика, а політика довіри до сюїти (докладніше про розкладку — у главі про shift-left цього розділу і в розділі «Git і CI/CD»). Поруч живуть анотації test.skip, test.fixme, test.fail — вони не про фільтрацію, а про свідоме виключення чи очікуваний провал конкретного тесту. У CodeceptJS теги вішають на сценарій (Scenario('...', ...).tag('@smoke') або @smoke у назві) і так само відбирають через --grep.
Шлях від коду до звіту
Складемо все докупи. Що відбувається між npx playwright test і зеленою чи червоною цифрою в CI:
Дві точки на цьому шляху варто закарбувати. По-перше, артефакти народжуються з провалу: скріншот, відео і trace ранер зазвичай зберігає саме тоді, коли тест упав, — це і є докази для . По-друге, репортер віддає результат у двох виглядах: HTML — для людини, а JUnit XML — для машини. Саме JUnit XML є форматом обміну з CI: з нього дашборд дізнається, скільки тестів упало, і чи блокувати . Наскільки широко його читають конкретні CI-системи, джерела глави не кажуть — це перевіряють у доці своєї CI. «Звіт як продукт» — окрема канонічна тема глави /avtomatyzatsiia/zvitnist-artefakty-triazh-trendy.
Типові помилки
- Виглядає як баг застосунку, а насправді — протік стану. не знаходить елемент, тест червоний — але причина не в продукті, а в тому, що попередній тест лишив дані через спільну
beforeAll-фікстуру. Перевір ізоляцію раніше, ніж заводити баг. - Виглядає, що застосунок «повільний», а насправді — переплутані рівні таймаутів. Тест «падає за 5 секунд при timeout 30» — це expect timeout однієї перевірки, а не таймаут тесту.
- Виглядає, що
expectчекає, а насправді немає.expect(await locator.isVisible()).toBe(true)вирвав значення до перевірки — автоочікування вимкнене власноруч. - Виглядає, що
afterEachприбере дані, а насправді setup упав на півдорозі. Прибирання не знає, що встигло створитися. Аргумент за фікстури, де setup і teardown зшиті. - Виглядає, що тег працює, а насправді grep нічого не матчить. Друкарська помилка в тезі або не той синтаксис — і фільтр тихо ганяє все або нічого. Перевіряй, скільки тестів реально відібралось.
- Виглядає, що конфіг застосувався, а насправді env-змінна не підвантажилась. Немає dotenv чи інша назва змінної — тест пішов на дефолтний
localhostі «загадково» червоний саме в CI.
Підсумок
- Ранер оркеструє й виносить вердикт; тест провалений, коли з нього вилітає виняток. Бібліотека асертів кидає цей виняток при розбіжності.
beforeAll— раз на скоуп,beforeEach— на кожен тест. Спільний мутабельний стан уbeforeAll= залежність від порядку й крихка сюїта.- Фікстура — це setup і teardown в одному місці, на вимогу й композовна; для ізоляції та ресурсів вона надійніша за хуки, а скоуп (
testvsworker) керує ціною ізоляції. - Конфіг виносить оточення,
baseURLі таймаути з коду; таймаути ієрархічні, і плутати їхні рівні — це ставити хибний діагноз. - Теги — це політика фільтрації (smoke на PR, регресія вночі), а шлях до звіту закінчується
JUnit XML, який читає CI.
Можливі питання
- «Чим
beforeAllвідрізняється відbeforeEach? Коли який брати?» Дивляться, чи розумієш ізоляцію й ризик спільного стану, а не лише «один раз проти щоразу». - «Що таке фікстура і чим вона краща за
beforeEach?» Сильна відповідь: setup+teardown разом, на вимогу, композиція через залежності, керований скоуп. - «Які бувають рівні таймаутів?» Тест, expect, дія, навігація — і чи не сплутаєш, коли який спрацював.
- «Тест зелений локально, червоний у CI — з чого почнеш?» Оточення й конфіг, env-змінні та
baseURL, потім артефакти й trace. Тема-місток до й до CI. - «Hard vs soft assertions? Що таке web-first assertion?» Перевіряють розуміння, що перевірка може повторюватись і що зупиняє тест.
- «Як зробити, щоб smoke ганявся на PR, а повна регресія — вночі?» Теги плюс
--grepплюс розкладка в CI. - «Куди дівається результат тесту — як CI дізнається, що впало?» Репортер і
JUnit XMLяк спільний формат.
Джерела
Тестовий стек — це не один інструмент, а кілька шарів
- ISTQB® CTAL-TAE Syllabus v2.0 — §3.1.1, §3.1.3: фреймворк автоматизації включає тестовий гарнес (він же ранер), бібліотеки, скрипти й сюїти; шари — test scripts → business logic → core libraries, і кількість шарів рекомендовано тримати низькою.
- CodeceptJS — Configuration — приклад стека, зібраного з деталей: хелпер задає рушій виконання (
Playwright,WebDriver,Puppeteer,Appium), а конфіг слугує ще й реєстром впровадження залежностей.
Ранер: оркестратор, який виносить вердикт
- ISTQB® CTAL-TAE Syllabus v2.0 — §3.1.1: gTAA відокремлює визначення тесту від SUT і від інструментів, а виконання й логування названо окремою здатністю рішення — це і є роль ранера.
- Playwright — Parallelism — бік розкладки по воркерах: у повністю паралельному режимі тести йдуть в окремих воркерах і не можуть ділити стан.
Бібліотека асертів: перевірка, що вміє падати
- Playwright — Assertions — поділ матчерів на ретрайні й неретрайні (саме через неретрайні тест і стає флакі) і поведінка
expect.soft: виконання не зупиняє, але тест позначає проваленим. - Playwright — Best Practices — web-first асерт чекає й повторює перевірку, а
expect(await locator.isVisible()).toBe(true)не чекає жодної секунди. - ISTQB® CTAL-TAE Syllabus v2.0 — §6.1.1: автоматизувати треба і виконання, і перевірку — порівнянням конкретних елементів фактичного результату з очікуваним.
Хуки життєвого циклу і їхні пастки
- Jest — Setup and Teardown — чотири хуки й різниця «на кожен тест проти раз на файл»; хуки верхнього рівня діють на всі тести файлу, а порядок жорсткий: зовнішній
beforeEachвиконується раніше за вкладений. - Playwright — Best Practices — хуки названо легітимним способом уникнути повторення, і сам по собі він ізоляції не порушує.
- Playwright — Parallelism — пастка спільного стану: у паралельному режимі тести не можуть ділити глобальні змінні, а
beforeAll/afterAllкожен воркер виконує сам для себе.
Фікстури: setup і teardown в одному місці, на вимогу
- Playwright — Test fixtures — три властивості, яких немає в хуків (інкапсуляція setup і teardown, між файлами, on-demand), і скоупи: тестова фікстура прибирається після кожного тесту, воркерна — коли завершується воркер, і має власний таймаут.
- ISTQB® CTAL-TAE Syllabus v2.0 — §4.2: фікстура задає стан оточення перед тестом і прямо повʼязана з повторюваністю й атомарністю.
- xUnit Test Patterns — Fresh Fixture — вибір «будувати фікстуру щоразу заново чи перевикористовувати» названо ключовим рішенням автоматизації — це і є компроміс
testпротиworker.
Конфігурація: оточення, base URL, таймаути
- Playwright — Test configuration —
baseURL, таймаут тесту й окремий таймаутexpect,projects; там же межа, про яку застерігає сама дока: опції ранера живуть на верхньому рівні конфігу, а опції оточення тесту — в секціїuse(таймаути дії та навігації ця сторінка не описує). - CodeceptJS — Configuration — той самий винос оточення в конфіг в іншій моделі: хелпер задає рушій, а
includeперелічує обʼєкти, які інжектяться в сценарій за іменем.
Теги й фільтрація: політика, а не декор
- Playwright — Annotations and tags — теги й відбір через
--grep, а поруч — несуча різниця анотацій:failтест запускає й вимагає, щоб він упав, аfixmeне запускає взагалі; анотації можуть бути умовними.
- Playwright — Reporters — два вигляди результату:
JUnit-XML як формат обміну для CI-систем і HTML як самодостатня тека, яку можна віддати вебсторінкою. - Playwright — Trace viewer — як інструмент
post-mortem-розбору саме для падінь у CI: локатор і тривалість кожної дії, DOM-знімки до й після, мережа й . - ISTQB® CTAL-TAE Syllabus v2.0 — §6.1.1: у разі падіння рішення автоматизації має зберегти все, що потрібно для аналізу, а статус
failedвимагає додаткової інформації про причину.
Відео до теми
Що таке ранер (test runner) і за що він відповідає?
— це оркестратор прогону: сам він нічого не перевіряє, а керує всім навколо тесту. Він знаходить файли за маскою (типово *.spec.ts), будує з describe/test дерево кейсів, розкладає їх по , у правильному порядку запускає хуки й , ізолює кожен тест і збирає вердикти для репортера. Тіло самого тесту — звичайна , яку ранер викликає. Junior часто плутає ранер із бібліотекою браузерної автоматизації, хоча це різні шари: ранер диригує, а клацає й вводить текст уже драйвер. Приклади ранерів — Playwright Test, Jest, Vitest, Mocha.
З яких шарів складається тестовий стек і навіщо їх розрізняти?
За словом «Playwright» чи «CodeceptJS» ховається не один інструмент, а кілька окремих ролей: ранер (оркестрація), бібліотека (формулює перевірку й кидає помилку), драйвер/адаптер (керує браузером чи транспортом), хуки й фікстури (готують і прибирають оточення), конфігурація (виносить параметри) та репортер (робить звіт). Розрізняти їх важливо, бо на різних проєктах стек зібраний по-різному: Playwright і Cypress дають усі шари одним пакетом, а класична зв'язка Mocha + Chai + WebdriverIO склеюється з окремих деталей. Коли на співбесіді питають «чим Playwright відрізняється від Selenium», половина відповіді — саме про те, які шари інструмент бере на себе, а які лишає тобі. Той, хто бачить шари, легко читає чужий стек; той, хто ні, копіює приклади не розуміючи, чому вони працюють.
Як ранер вирішує, що тест провалився?
Ранер вважає тест провальним тоді, коли з його функції вилітає виняток (exception) — і ніяк інакше. Немає окремого «статусу fail», який тест сам собі проставляє: дійшли до кінця функції без винятку — passed, десь усередині щось кинуло помилку — failed. Це проста, але фундаментальна модель, бо вона пояснює, звідки взагалі береться вердикт. Найчастіше той виняток кидає бібліотека асертів, коли фактичне значення не збіглося з очікуваним. Зрозумівши це, junior перестає дивуватися, чому «тест зелений, хоча перевірка насправді не відпрацювала» — часто перевірку просто забули заавейтити, і виняток не встиг вилетіти.
Що таке бібліотека асертів і як вона пов'язана з вердиктом?
Бібліотека асертів — це шар, що формулює перевірку й кидає помилку при розбіжності. expect тут працює як фасад над набором матчерів (matchers): toBe, toEqual, toHaveText, toBeVisible. Механізм елементарний: матчер порівнює фактичне з очікуваним і, якщо вони різні, кидає AssertionError. Ранер ловить цю помилку й фарбує тест червоним. Тобто «перевірка» в голові тестувальника перетворюється на «вердикт» у звіті саме через виняток, який кидає матчер, — це і є весь зв'язок між assertion і статусом тесту.
Чим web-first assertion у Playwright відрізняється від звичайної перевірки?
Звичайний асерт перевіряє значення один раз, тут і зараз: expect(2).toBe(2) порівнює й одразу дає вердикт. Web-first assertion (наприклад expect(locator).toBeVisible()) поводиться інакше — вона повторює перевірку в циклі, поки умова не стане істинною або поки не вичерпається expect timeout. Це вбудований захист від гонок: елемент, який відрендериться за 200 мілісекунд, не завалить тест, бо перевірка дочекається його появи. Практичний наслідок — з web-first assertions рідше потрібні ручні очікування, а від «не встиг з'явитися» стає значно менше. Важливо, що це властивість саме перевірок над , а не будь-якого expect.
Чому expect(await locator.isVisible()).toBe(true) — антипатерн?
Тому що ти власноруч викидаєш й перетворюєш web-first перевірку на одноразову. Спочатку await locator.isVisible() виконується один раз і повертає булеве значення (наприклад false, бо елемент ще не встиг з'явитися), а вже потім toBe(true) порівнює цей застиглий результат — і одразу падає, немає. Значення «вирвано» з часу до того, як перевірка встигла щось дочекатися. Правильний варіант — await expect(locator).toBeVisible(): тут перевірка сама циклічно чекає на видимість до вичерпання . Це один із найтиповіших генераторів в junior-коді.
Чим hard-перевірка відрізняється від soft і коли брати soft?
Звичайний expect — hard: при першій же розбіжності він кидає виняток і зупиняє тест, решта перевірок не виконуються. expect.soft — soft: він фіксує провал, але не перериває тест, тож той доходить до кінця й збирає всі порушення за один прогін. Soft зручний, коли на одній сторінці треба незалежно перевірити багато полів і хочеться побачити відразу весь список розбіжностей, а не ганяти прогін по колу, лагодячи по одній. Але soft не робить тест «незалежним» від логіки: якщо після провальної soft-перевірки код фізично не може продовжитись (наприклад, елемента немає), далі все одно впаде. Тому soft — про повноту діагностики, а не про те, щоб ігнорувати провали.
Чим beforeAll відрізняється від beforeEach і коли який брати?
Різниця — у частоті: beforeAll виконується раз перед усіма тестами (у Playwright — раз на файл у межах воркера), а beforeEach — перед кожним тестом окремо. Симетрично afterAll спрацьовує раз наприкінці, а afterEach — після кожного тесту, навіть після провального. Правило вибору просте: усе, що є спільним і незмінним (наприклад, підняти сервер, прочитати конфіг), можна класти у beforeAll; усе, що змінюється або має бути свіжим для кожного тесту (, стан застосунку), — тільки у beforeEach або фікстуру. Сильна відповідь тут — не про «один раз проти щоразу», а про ізоляцію: спільний мутабельний стан у beforeAll робить тести залежними від порядку.
Чому прибирання вішають в afterEach, а не в кінці тіла тесту?
Бо afterEach виконується навіть тоді, коли тест упав, а до кінця тіла впалий тест просто не доходить. Якщо покласти останнім рядком тесту, то будь-який виняток вище нього залишить дані неприбраними — і наступні тести отримають забруднене середовище. afterEach же ранер викликає гарантовано, незалежно від вердикту, тому саме туди природно вішати видалення створених записів, закриття з'єднань, скидання стану. Це робить прибирання надійним, а не «якщо пощастить дійти». Той самий принцип у фікстурах реалізовано ще акуратніше — через код після use.
Чим небезпечний спільний мутабельний стан у beforeAll?
Він убиває незалежність тестів. Спокуса створити користувача чи відкрити з'єднання один раз і користуватися ними скрізь економить секунди, але щойно один тест змінить цей стан — наступний отримає його зіпсованим, і тести починають залежати від порядку виконання. Поки вони йдуть послідовно й у фіксованому порядку, проблема ховається; варто ввімкнути рандомізацію чи паралель — і розсипається «на рівному місці». Симптом упізнаваний: тест зелений поодинці, але червоний у повному наборі, а результат залежить від порядку запуску. Тому стан, що змінюється, має жити на рівні beforeEach або фікстури, а незмінні дорогі ресурси — окремо й свідомо.
Що станеться, якщо виняток вилетить у beforeAll?
Він завалить усі тести файлу разом. Якщо підготовка на рівні файлу впала, ранер не має середовища для жодного тесту скоупу, тож усі вони позначаються як провалені або пропущені. Один крихкий крок у beforeAll — і замість одного зрозумілого падіння отримуєш десяток червоних тестів, серед яких справжню причину видно не одразу. Це ще один аргумент тримати beforeAll максимально тонким і надійним: чим більше в ньому крихкої логіки, тим ширший радіус ураження при збої. Для корисно знати цей патерн: масове падіння всього файлу — привід одразу дивитися в його setup, а не в кожен тест окремо.
Що таке фікстура і чим вона краща за beforeEach?
Фікстура (fixture) — це іменований шматок оточення тесту, у якому setup і teardown зшиті докупи й видаються тесту на вимогу. Тест просто оголошує в параметрах, що йому потрібен, скажімо, authedPage, і отримує вже залогінену сторінку; звідки вона взялась і хто її прибере — не його турбота. Перевага перша: setup і teardown лежать поруч (усе до use — підготовка, усе після — прибирання), тож немає розсинхрону, коли teardown не знає, що встиг створити setup. Перевага друга — lazy: фікстура запускається, тільки якщо тест її попросив, тоді як beforeEach відпрацьовує перед кожним тестом безвідмовно. Перевага третя — композиція: фікстури залежать одна від одної, і ранер сам вибудовує порядок; ми читаємо це як (dependency injection) за іменем параметра, хоч сама дока Playwright цього терміна не вживає.
У чому суть скоупу фікстури test проти worker?
Скоуп визначає, як часто фікстура створюється й прибирається. За замовчуванням фікстура має test-скоуп: вона живе рівно один тест — це максимальна ізоляція, кожен кейс отримує свіже оточення. Але дорогі ресурси (піднятий сервер, з'єднання з БД) розумно робити worker-скоупними: тоді вони створюються раз на воркер і перевикористовуються всіма його тестами. Компроміс очевидний: worker-скоуп економить час, але повертає спільного стану — усе, що ми казали про beforeAll, стосується і worker-фікстур. Тому вибір скоупу — це свідомий баланс «ціна ізоляції проти швидкості», а не технічна дрібниця.
Навіщо виносити baseURL у конфіг замість того, щоб писати повну адресу в тесті?
Бо прибите цвяхами значення на кшталт page.goto('https://staging.example.com/login') прив'язує тест до одного середовища: локально чи на проді той самий тест уже не запустиш без правок коду. Коли адресу задано один раз через baseURL, а в тестах пишуть відносні шляхи (page.goto('/login')), середовище перемикається однією змінною оточення — код тестів лишається незмінним. У CI цю змінну підкладають зі сховища секретів, а локальний дефолт через ?? рятує запуск на машині розробника. Це і про , і про безпеку: реальні адреси й секрети не осідають у git. Плюс так конфіг стає єдиним місцем правди про те, куди взагалі бити тестам.
Які бувають рівні таймаутів і чому їх плутати небезпечно?
Таймаути ієрархічні, і це різні речі: test timeout обмежує, скільки живе весь тест цілком; expect timeout — скільки одна web-first перевірка повторює спробу; action timeout — скільки чекає одна дія (click, fill); navigation timeout — скільки чекає перехід на сторінку. Перші два задаються прямо в конфізі (timeout, expect.timeout) і описані на сторінці конфігурації Playwright; таймаути дії та навігації — окремі опції, і їхні імена варто звіряти з докою своєї версії інструмента. Плутати рівні — значить ставити хибний діагноз. Класика: «тест падає за 5 секунд, хоча я дав йому 30» — тут 30 секунд це test timeout на весь тест, а впала конкретна перевірка по своєму 5-секундному expect timeout. Побачивши падіння по таймауту, спершу треба зрозуміти, який саме таймаут спрацював, і лише тоді крутити відповідне число. Наосліп піднімати test timeout, коли впав expect, — марно.
Що таке projects у конфізі Playwright?
projects — це важіль, що множить один і той самий набір тестів на матрицю середовищ чи браузерів. Кожен «проєкт» — це запис із власним блоком use: наприклад, один і той самий сценарій ганяється як проєкт chromium і проєкт webkit, і різняться вони лише параметрами оточення. Так по кількох браузерах не потребує дублювання тестів — ранер сам проганяє їх під кожен профіль. Через projects зручно розділяти й середовища чи набори налаштувань (мобільна проти десктопної). Це частина того, як конфіг виносить варіативність із коду тестів у декларативний опис.
Що таке теги й навіщо --grep у прогонах?
Тег — це мітка (у Playwright — рядок на кшталт @smoke у назві тесту чи в опції tag), за якою ранеру кажуть, що саме запускати. Коли тестів сотні, ганяти всі на кожен пуш стає дорого, і тут вмикається фільтрація: npx playwright test --grep @smoke відбере лише позначені. Практична цінність — розкладка прогонів: швидкий @smoke ганяють на кожен PR, а повну — вночі за розкладом. Фільтрувати можна не тільки за тегом, а й за файлом, фрагментом назви чи проєктом. Це не косметика, а політика довіри до сюїти: чим осмисленіша фільтрація, тим швидший зворотний зв'язок розробнику без втрати покриття.
Тест зелений локально, а червоний у CI — з чого почнеш діагностику?
Найперше — оточення й конфіг, бо саме тут найчастіше й криється різниця. Перевіряю env-змінні та baseURL: чи не пішов тест у CI на дефолтний localhost через те, що dotenv не підвантажився або змінна названа інакше — «загадкова» червоність саме в майже завжди звідси. Далі — секрети й доступи: чи доїхав токен, чи те середовище взагалі піднялося. Тільки потім лізу в артефакти прогону: , відео і trace показують, що реально бачив тест у момент падіння. Окремо тримаю в голові патерн паралелізму: якщо поодинці зелено, а в CI під воркерами плаває, підозра зміщується на ізоляцію стану, гонки чи rate limit. Дисципліна проста — не заявляти причину без доказу з артефактів чи логів.
Куди дівається результат тесту — як CI дізнається, що саме впало?
Вердикти від ранера підхоплює репортер і віддає їх у двох виглядах: HTML — для людини (зручно клікати й дивитися артефакти) і JUnit XML — для машини. Саме JUnit XML є форматом обміну з CI: з нього дашборд дізнається, скільки тестів упало, які й чи блокувати (наскільки широка ця підтримка в конкретних системах — питання до доки вашої CI, джерела глави про це не кажуть). Тобто ланцюг такий: тест кинув (чи не кинув) виняток, ранер зафіксував вердикт, репортер серіалізував його в XML, а гейт у пайплайні прочитав XML і вирішив долю збірки. Ще одна деталь, яку варто закарбувати: артефакти народжуються з провалу — скріншот, відео і trace ранер зазвичай зберігає саме тоді, коли тест упав, і це головні докази для тріажу. Без розуміння цього ланцюга «CI сказав, що впало» звучить як магія, хоча за ним проста серіалізація вердиктів.
Три кейси, де розуміння стека відрізняє швидкий фікс від багатогодинного дебагу інфраструктури: , що розсипається під паралеллю через спільний стан у beforeAll; , яка знімає розсинхрон setup/teardown і вмикається на вимогу; і читання падіння по , коли важливо не сплутати рівні. Скрізь — що дивитися і чому.
Кейс 1. beforeAll протікає: зелено поодинці, червоно в наборі
Симптом класичний: локально в один потік усе зелене, а в CI під частина тестів «випадково» червоніє, і результат залежить від порядку запуску. Ось код, який це породжує:
import { test, expect } from '@playwright/test';
// ПОГАНО: один користувач на весь файл, стан спільний і мутабельний
let userBalance = 100;
test.beforeAll(async () => {
userBalance = 100; // «скидаємо» раз на файл
});
test('списання зменшує баланс', async () => {
userBalance -= 30;
expect(userBalance).toBe(70);
});
test('баланс новачка дорівнює 100', async () => {
expect(userBalance).toBe(100); // впаде, якщо перший тест уже відпрацював
});
Поки тести йдуть у написаному порядку й послідовно, другий іноді проскакує. Але варто запустити їх у різному порядку або на різних воркерах — і userBalance приходить у другий тест уже зіпсованим. Виправлення — перенести мутабельний стан на рівень кожного тесту:
import { test, expect } from '@playwright/test';
test('списання зменшує баланс', async () => {
let balance = 100; // свій стан у кожного тесту
balance -= 30;
expect(balance).toBe(70);
});
test('баланс новачка дорівнює 100', async () => {
const balance = 100;
expect(balance).toBe(100);
});
Що дивитися і чому:
- Залежність від порядку — головний симптом. Якщо перестановка тестів місцями чи вмикання паралелі змінює вердикт, шукай спільний мутабельний стан, а не баг продукту.
beforeAllтут — перший підозрюваний. beforeAllгодиться лише для незмінного. Підняти сервер, прочитати конфіг — так; створити дані, які тести змінюватимуть, — ні. Усе, що мутує, живе уbeforeEachабо фікстурі.- Радіус ураження ширший, ніж здається. Якби «скидання» балансу впало у
beforeAll, червоними стали б усі тести файлу одразу — і справжню причину довелося б шукати серед десятка однакових падінь.
Кейс 2. Фікстура замість пари beforeEach/afterEach
«Підготувати в beforeEach, прибрати в afterEach» виглядає симетрично, але крихке: якщо beforeEach кинув виняток на півдорозі (створив половину даних), afterEach уже не знає, що саме встигло створитися. Setup і teardown лежать у різних місцях і нічого не знають одне про одного. Фікстура зшиває їх в одне місце й видає на вимогу:
import { test as base, type Page } from '@playwright/test';
type Fixtures = { authedPage: Page };
export const test = base.extend<Fixtures>({
authedPage: async ({ page }, use) => {
await page.goto('/login'); // setup
await page.getByLabel('Email').fill('user@example.com');
await page.getByRole('button', { name: 'Увійти' }).click();
await use(page); // віддали залогінену сторінку тесту
await page.context().clearCookies(); // teardown — код ПІСЛЯ use
},
});
Тест просто просить authedPage у параметрах і отримує вже готову сторінку:
import { expect } from '@playwright/test';
import { test } from './fixtures';
test('кабінет доступний залогіненому', async ({ authedPage }) => {
await authedPage.goto('/dashboard');
await expect(authedPage.getByRole('heading')).toHaveText('Кабінет');
});
Дорогий ресурс варто підняти раз на воркер, а не на кожен тест — це задає :
export const test = base.extend<{}, { server: Server }>({
server: [async ({}, use) => {
const server = await startServer(); // раз на воркер
await use(server);
await server.close();
}, { scope: 'worker' }],
});
Що дивитися і чому:
- Межа проходить по
use. Усе доuse— підготовка, усе після — , яке ранер викличе автоматично навіть при падінні тесту. Розсинхрону немає: teardown бачить те саме, що створив setup. - Фікстура lazy,
beforeEach— ні. Фікстура запускається, лише якщо тест назвав її в параметрах;beforeEachвідпрацьовує перед кожним тестом безвідмовно, навіть коли результат конкретному тесту не потрібен. scope: 'worker'економить, але повертає . Один сервер на воркер — швидше, проте це знову спільний стан: усе, що написано в кейсі 1 проbeforeAll, стосується і worker-фікстур. Скоуп — це свідомий вибір «ціна ізоляції проти швидкості».- Композиція безкоштовна.
authedPageспирається на вбудовануpage, ранер сам вибудовує порядок — це за іменем параметра.
Кейс 3. Падіння по таймауту: не сплутати рівні
«Тест падає за 5 секунд, хоча я дав йому 30» — щоденна плутанина. Розберемо на конфізі, де таймаути різних рівнів задані окремо (сторінка конфігурації Playwright описує лише таймаут тесту й expect, тож точні імена опцій для дії та навігації звіряй із докою своєї версії інструмента):
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
actionTimeout: 10_000, // одна дія: click, fill
navigationTimeout: 15_000, // перехід на сторінку
},
timeout: 30_000, // весь тест цілком
expect: { timeout: 5_000 }, // одна web-first перевірка
});
Тепер тест «падає за 5 секунд» на такому рядку:
// перевірка циклічно чекає видимості й здається через свій 5-секундний expect timeout
await expect(page.getByRole('alert')).toBeVisible();
Повідомлення в звіті прямо називає винуватця:
Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
Що дивитися і чому:
- 30 секунд тут ні до чого.
timeout: 30_000обмежує весь тест, а впала конкретна перевірка по своємуexpect.timeoutу 5 секунд. Піднімати test timeout — марно: перевірка все одно здасться на п'ятій секунді. - Спочатку читай, який таймаут спрацював. У тексті помилки видно рівень:
expect(...).toBeVisible()— це expect timeout;waiting for locator.click()— action timeout;page.goto— navigation timeout. Крутити треба саме те число, яке назвала помилка. - Хибний діагноз коштує годин. Прийняти expect timeout за «повільний застосунок» і піти оптимізувати бекенд — типова помилка; реальна причина може бути в тому, що елемент просто не з'являється, і жоден таймаут його не дочекається.
- Дефолт
baseURLчерез??— окрема страховка. ЯкщоBASE_URLне підвантажився (немає dotenv чи інша назва змінної), тест піде наlocalhostі «загадково» червонітиме саме в CI — це вже інший діагноз, але з тієї ж родини «конфіг не такий, як здається».
Шари стека і ранер
- Можу назвати шари тестового стека (, бібліотека , драйвер, хуки/, конфіг, репортер) і пояснити, за що кожен відповідає.
- Розумію, що ранер сам нічого не перевіряє — він знаходить тести, розкладає по , запускає хуки/фікстури й збирає вердикти.
- Знаю, що Playwright/Cypress дають усі шари одним пакетом, а Mocha + Chai + WebdriverIO склеюється з окремих деталей.
Вердикт і бібліотека асертів
- Знаю, що тест провалений тоді й тільки тоді, коли з його функції вилітає виняток — окремого «статусу fail» немає, а кидає його зазвичай матчер через
AssertionError. - Розумію різницю між звичайною перевіркою (один раз) і web-first assertion (повторює спробу до expect timeout).
- Можу пояснити, чому
expect(await locator.isVisible()).toBe(true)вимикає , аawait expect(locator).toBeVisible()— ні. - Знаю різницю hard vs soft:
expectзупиняє тест при першій розбіжності,expect.softдає зібрати всі порушення за прогін.
Хуки життєвого циклу
- Знаю різницю
beforeAll(раз на ) vsbeforeEach(перед кожним тестом) і коли який брати. - Можу пояснити, чому вішають в
afterEach, а не в кінець тіла тесту (впалий тест до кінця не доходить). - Розумію, чому спільний мутабельний стан у
beforeAll= залежність від порядку й розсипання під паралеллю. - Знаю, що виняток у
beforeAllвалить усі тести файлу, і вмію впізнати цей патерн у .
Фікстури і скоуп
- Можу пояснити, що фікстура — це setup і teardown в одному місці, на вимогу (lazy) і композовна (DI за іменем параметра).
- Знаю, де в коді фікстури проходить межа: усе до
use— підготовка, усе після — прибирання. - Знаю різницю скоупів
test(свіже оточення на кожен тест) vsworker(раз на воркер) і компроміс «ізоляція проти швидкості».
Конфігурація і таймаути
- Розумію, чому прибиту в коді адресу виносять у
baseURLз відносними шляхами в тестах, і як середовище перемикається однією env-змінною. - Знаю чотири рівні (test, expect, action, navigation) і що в конфізі Playwright описані перші два, а решта — окремі опції з доки; не сплутаю, який спрацював, і читаю «падає за 5 с при timeout 30» як expect timeout.
- Розумію, що
projectsмножить один набір тестів на матрицю браузерів/середовищ без дублювання коду.
Теги, звіт і CI
- Можу пояснити теги як політику фільтрації (
@smokeна PR, вночі) і що--grepфільтрує за тегом/назвою/файлом. - Знаю, що артефакти (, відео, trace) народжуються з провалу й потрібні для тріажу.
- Можу пояснити роль
JUnit XMLяк формату обміну з CI: з нього дізнається, що впало й чи блокувати .
Квіз
Перед стартом
- Питань: 14
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Що таке ранер (test runner)?

