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) описує один кейс. Тіло тесту — звичайна .

    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) за іменем параметра. Сама дока 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:

    Так

    Ні

    Конфіг: оточення, 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 є форматом обміну з 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 в одному місці, на вимогу й композовна; для ізоляції та ресурсів вона надійніша за хуки, а скоуп (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 як спільний формат.

    Джерела

    Тестовий стек — це не один інструмент, а кілька шарів

    • 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 configurationbaseURL, таймаут тесту й окремий таймаут 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 вимагає додаткової інформації про причину.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.