vyvchy
    Теми розділу

    06 · Автоматизація: стратегія

    Тестовий стек зсередини: ранер, фікстури, хуки, конфігурація

    Зміст

    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)Перетворює вердикти на звіт для людини й CIHTML, 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) описує один кейс. Тіло тесту — звичайна async-функція.

    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 — раз наприкінці.

    beforeAll
    раз на файл

    beforeEach

    Тіло тесту 1

    afterEach

    beforeEach

    Тіло тесту 2

    afterEach

    afterAll
    раз наприкінці

    beforeAll
    раз на файл

    beforeEach

    Тіло тесту 1

    afterEach

    beforeEach

    Тіло тесту 2

    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) за іменем параметра.

    Окрема важлива вісь — скоуп (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Скільки чекає перехід на сторінку

    Плутати їх — значить ставити хибний діагноз. «Тест падає за 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:

    Так

    Ні

    Конфіг: оточення, baseURL, таймаути

    Пошук тестів .spec.ts

    Ранер розкладає по воркерах

    Фікстури й хуки: setup

    Тіло тесту + перевірки

    Виняток?

    Артефакти: скриншот, відео, trace

    passed

    failed

    Репортер

    HTML-звіт + JUnit XML

    CI: тренди, гейти

    Так

    Ні

    Конфіг: оточення, baseURL, таймаути

    Пошук тестів .spec.ts

    Ранер розкладає по воркерах

    Фікстури й хуки: setup

    Тіло тесту + перевірки

    Виняток?

    Артефакти: скриншот, відео, trace

    passed

    failed

    Репортер

    HTML-звіт + JUnit XML

    CI: тренди, гейти

    Дві точки на цьому шляху варто закарбувати. По-перше, артефакти народжуються з провалу: скриншот, відео і trace ранер зазвичай зберігає саме тоді, коли тест упав, — це і є докази для тріажу. По-друге, репортер віддає результат у двох виглядах: HTML — для людини, а JUnit XML — для машини. Саме JUnit XML є спільною мовою (lingua franca), яку розуміють майже всі 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 в одному місці, на вимогу й композовна; для ізоляції та ресурсів вона надійніша за хуки, а скоуп (test vs worker) керує ціною ізоляції.
    • Конфіг виносить оточення, 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 як спільний формат.

    Джерела

    • Playwright — Test fixtures — фікстури, test.extend, скоупи test/worker.
    • Playwright — Test configurationbaseURL, рівні таймаутів, projects.
    • Playwright — Annotations and tags — теги, --grep, skip/fixme/fail.
    • Jest — Setup and Teardown — хуки життєвого циклу і порядок їх виконання.
    • CodeceptJS — Configuration — конфіг, хелпери, оточення.
    • ISTQB — силабус CTFL 4.0 торкається інструментів лише концептуально (розділ «Підтримка тестування інструментами»); практична глибина автоматизації — у силабусі Certified Tester Test Automation Engineering (CT-TAE).