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

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

    Тест-дані: фабрики, фікстури, ізоляція

    Зміст

    Автотест — це не лише кроки й перевірки. Перш ніж клікнути «Оплатити», хтось має створити користувача, товар і кошик із грошима на балансі. Це «щось» і є тестові дані (test data), а стан (state), у який ви приводите застосунок перед перевіркою, — половина тесту. Коли дані брудні, спільні або непередбачувані, тест червоніє там, де продукт справний, або зеленіє там, де є регресія. Дані — найтихіший руйнівник стабільності сьюти: їх не видно в кроках, але саме через них тест «іноді» падає.

    Це канонічна глава про стратегію тест-даних — інші глави (флакі-тести, паралелізація, тести API і БД) посилаються сюди. Тема має бейдж «поглиблення»: при першому проході автоматизації її можна відкласти й повернутися, коли ваші тести почнуть заважати одне одному або червоніти без змін у продукті. Але на співбесіді на middle і вище її питають майже завжди — бо вміння тримати дані під контролем відрізняє того, хто пише тести, від того, хто пише стабільні тести.

    Фікстури, стан і передумови: словник

    Три слова, які плутають. Фікстура (fixture) — це фіксоване, відоме оточення, у якому виконується тест: набір даних, залогінений користувач, піднятий сервіс. Історично фікстура — це саме файл із заготовленими даними; у сучасних ранерах (Playwright, pytest) фікстурою називають ще й механізм, що готує це оточення й прибирає його по завершенні. Стан (state) — конкретна ситуація в системі на момент перевірки: «є замовлення в статусі paid». Передумова (precondition) — те, що має бути істинним ще до першого кроку тесту.

    Ключова думка, до якої ми повертатимемося: arrange-фаза автотесту (див. Анатомію автотесту) — це і є підготовка даних та стану. Питання не «чи готувати», а «звідки брати» і «як не дати їм протекти між тестами».

    Статичні фікстури проти генерації

    Найпростіший спосіб мати дані — покласти їх у файл: users.json, products.json, дамп у репозиторії. Тест читає файл, підставляє значення. Це статична фікстура.

    Її сила — детермінованість і читабельність: у файлі видно рівно ті значення, на яких тест зелений. Її слабкість — старіння й колізії. Захардкоджений user@example.com працює, поки тест один. Щойно два тести (або два воркери) створюють користувача з тим самим email — колізія унікального ключа, і другий падає з 409 Conflict. Статичний дамп ще й тихо протухає: сьогодні в ньому активна підписка, а через місяць бекенд змінив правила, і «активна» перетворилась на «прострочена».

    Протилежний підхід — генерувати дані на льоту: щоразу свіжий email, випадкове імʼя, унікальний ідентифікатор. Це прибирає колізії й старіння, але коштує детермінованості: коли значення різні щопрогону, важче зрозуміти, на яких саме даних тест упав, і легше проґавити edge-кейс, якого генератор випадково не дав.

    КритерійСтатична фікстураГенерація
    ДетермінованістьВисока (видно значення)Нижча (значення різні)
    Колізії під паралельЛегко: спільний ключМалоймовірні
    Старіння данихТихо протухаєЗавжди свіжі
    Edge-кейсиМожна закласти явноТреба додавати руками
    Читабельність тестуВисокаНижча

    Правильна відповідь — не «або-або», а розподіл ролей. Довідкові, стабільні дані (список країн, тарифні плани, конфігурація фіч) — статичні: вони й мають бути фіксованими. Транзакційні, унікальні сутності (користувачі, замовлення, документи) — генеровані, бо кожен тест потребує свіжий екземпляр. А небезпечну класику (порожній рядок, пробіли, 4-байтний unicode, гранична довжина) генератор випадково не видасть — її закладають явно, як окремі кейси (детальніше — у розділі про тест-дизайн).

    Фабрики і faker

    Коли даних треба багато й вони схожі, зʼявляється фабрика (factory) — функція, що повертає готовий обʼєкт із розумними дефолтами, дозволяючи перевизначити лише те, що важливо для конкретної перевірки. Це прямий родич патерна Builder і Object Mother (див. патерни в автоматизації).

    type User = { email: string; name: string; role: 'admin' | 'member'; balance: number };
    
    function buildUser(overrides: Partial<User> = {}): User {
      return {
        email: faker.internet.email(),
        name: faker.person.fullName(),
        role: 'member',
        balance: 0,
        ...overrides,
      };
    }
    
    // у тесті беремо дефолт і міняємо ЛИШЕ те, що впливає на перевірку
    const admin = buildUser({ role: 'admin', balance: 1000 });

    Чому це важливо, а не просто «менше коду»: фабрика робить тест читабельним за наміром. У виклику buildUser({ role: 'admin' }) видно, що для цієї перевірки суттєва тільки роль, а email та імʼя — шум. Без фабрики кожен тест тягне повний обʼєкт із десятьма полями, і читач не розуміє, які з них навмисні, а які випадкові.

    Значення для полів дає faker — бібліотека, що генерує реалістичні фейкові дані: імена, адреси, email, телефони, UUID. Реалістичність корисна: ідеальне test test бреше про те, як виглядатимуть справжні дані, а довге прізвище з апострофом чи нелатинські літери ловлять баги верстки й кодувань.

    Одна пастка faker, за яку чіпляються на співбесіді: випадковість не гарантує унікальності. faker.internet.email() теоретично може двічі видати той самий рядок, а на великій сьюті — обовʼязково видасть. Тому для ключів, що мусять бути унікальними, домішують гарантовано унікальний компонент (мітку часу, індекс воркера, UUID), а не покладаються на «навряд чи збіжиться».

    Зворотний бік випадковості — недетермінованість. Коли треба відтворити фейл, faker сідують:

    import { faker } from '@faker-js/faker';
    faker.seed(123); // однаковий сид -> однакова послідовність значень

    Сид перетворює «випадкове, але відтворюване» на кероване: у CI можна логувати сид і, отримавши червоний тест, повторити рівно ту саму послідовність даних локально. Це компроміс: детермінованість фікстури плюс різноманіття генерації.

    Як довести систему до потрібного стану

    Припустимо, тесту потрібне замовлення в статусі paid. Створити його можна чотирма шляхами, і вибір між ними — одне з найважливіших рішень у дизайні сьюти.

    Сам UI-флоу створення

    Не флоу, а його наслідок

    Так

    Стан недосяжний ззовні

    Потрібен стан перед перевіркою

    Що саме перевіряє тест?

    Через UI
    повільно, крихко, реалістично

    Є придатний API?

    Через API
    швидко, поважає інваріанти

    Бекдор або пряма БД
    найшвидше, обходить логіку

    Сам UI-флоу створення

    Не флоу, а його наслідок

    Так

    Стан недосяжний ззовні

    Потрібен стан перед перевіркою

    Що саме перевіряє тест?

    Через UI
    повільно, крихко, реалістично

    Є придатний API?

    Через API
    швидко, поважає інваріанти

    Бекдор або пряма БД
    найшвидше, обходить логіку

    Через UI. Тест сам клікає по формах, щоб створити передумову. Єдиний випадок, коли це виправдано, — коли сам процес створення і є обʼєктом перевірки. У всіх інших випадках готувати стан кліками — марнотратство: повільно, крихко (кожна зміна форми ламає чужі тести) і змішує підготовку з перевіркою.

    Через API. Той самий стан створюється HTTP-запитом до публічного API застосунку. Це золотий стандарт arrange-фази: на порядок швидше за UI, стабільніше (API міняється рідше за верстку) і, головне, проходить через ту саму бізнес-логіку, що й реальний користувач, — тож не створює станів, неможливих у продакшені. Логін теж роблять через API один раз на ран і перевикористовують стан сесії (storageState) — механіка описана у главі про автентифікацію та авторизацію.

    Пряма БД. INSERT прямо в таблицю. Найшвидший шлях і єдиний, коли стан узагалі недосяжний через API (наприклад, треба «зістарити» запис на 40 днів назад). Ціна висока: ви обходите всі інваріанти застосунку — валідацію, тригери, обчислювані поля — і можете створити стан, який реальний код ніколи не породив би. Такий тест зелений на неможливих даних. Плюс жорстке звʼязування зі схемою БД: міграція таблиці ламає підготовку. Пряму БД тримають як виняток, не як звичку (сідінг через БД проти API — детальніше в розділі про бази даних).

    Бекдор (backdoor). Спеціальний тестовий ендпоінт, який існує лише в тестових середовищах: POST /test/seed-order. Компроміс між API і БД — швидко й без клікання, але через контрольований код, а не сирий INSERT. Головна засторога: бекдор не повинен існувати в проді. Тестовий ендпоінт, що доїхав до продакшену, — це готова вразливість (обхід автентифікації, створення адмінів). Його вмикають лише за прапорцем середовища.

    Правило: готуй стан на найнижчому достатньому рівні. UI — тільки коли перевіряєш сам UI-флоу; API — за замовчуванням; БД і бекдор — коли інакше стан недосяжний, і свідомо, з розумінням, що ти обходиш логіку продукту.

    Ізоляція: на тест і на воркер

    Ізоляція — це гарантія, що дані одного тесту не бачить і не псує інший. Без неї сьюта детермінована рівно доти, доки виконується послідовно й в одному порядку; варто ввімкнути паралелізацію — і вона розсипається.

    Є два рівні, які плутають.

    Ізоляція на тест. Кожен тест створює власні дані в arrange і не залежить від того, що лишив попередній. Це прямий наслідок незалежності тестів: якщо тест B читає користувача, якого створив тест A, то B впаде, щойно ви запустите його окремо, зміните порядок або зашардуєте сьюту між машинами. Симптом такого прихованого звʼязування — «тест проходить у повній сьюті, але падає, коли ганяєш його самого».

    Ізоляція на воркер. Коли тести йдуть паралельно в N процесах-воркерах, їхні дані не мають колідувати між процесами. Два воркери, що реєструють user@test.local, — це 409 на другому. Розвʼязань два: або унікалізувати дані ключем воркера, або дати кожному воркеру власний набір (акаунт, tenant, простір імен).

    // унікальність ключем воркера й міткою часу
    const email = `user+${test.info().workerIndex}-${Date.now()}@test.local`;
    // один акаунт на воркер: створюється раз, живе весь прогін воркера
    export const test = base.extend<{}, { account: Account }>({
      account: [async ({}, use, workerInfo) => {
        const account = await createAccount(`w${workerInfo.workerIndex}@test.local`);
        await use(account);           // усі тести цього воркера працюють у ньому
        await deleteAccount(account.id); // прибирання по завершенні воркера
      }, { scope: 'worker' }],
    });

    Воркер-скоуп фікстура — компроміс між швидкістю (не створювати акаунт на кожен тест) і ізоляцією (воркери не перетинаються). Тонкий момент: якщо кілька тестів в одному воркері ділять акаунт, вони знову звʼязані в межах воркера — тому дані мутувати мають так, щоб не заважати одне одному (різні замовлення в одного юзера, а не один спільний кошик).

    Прибирання: after-хуки, TTL і одноразові акаунти

    Створили дані — маєте прибрати. Інакше тестова база пухне, унікальні ключі вичерпуються, а наступні прогони спотикаються об чужі залишки.

    Базовий інструмент — teardown у хуках (afterEach, afterAll) або у фікстурі після use. Але наївне «afterEach усе підмете» має дірки:

    • Тест упав посередині arrange — половина даних створена, а хук чистить за списком, якого немає.
    • Сам teardown кинув помилку — і решту не прибрало.
    • Воркер помер (таймаут, OOM) — хуки взагалі не виконались.

    тест упав / воркер помер

    setup: створити дані

    тест виконується

    teardown: after-хук прибирає

    дані лишились сиротами

    TTL / нічний клінап
    підмітає залишки

    тест упав / воркер помер

    setup: створити дані

    тест виконується

    teardown: after-хук прибирає

    дані лишились сиротами

    TTL / нічний клінап
    підмітає залишки

    Тому надійна стратегія двошарова: after-хук як основний прибиральник плюс страховка, що не залежить від успіху тесту. Страховкою буває:

    • TTL (time to live) — тестові сутності живуть обмежений час, а фоновий джоб (або сам застосунок) видаляє прострочене. Осиротілі дані самі зникають.
    • Нічний клінап — окремий скрипт за розкладом, що зносить усе тестове старше за N годин за домовленою ознакою (префікс email test+, окремий tenant).
    • Одноразові акаунти — замість чистити стан, щоразу створюють свіжий акаунт і не переймаються його вмістом; прибирає їх той самий TTL-джоб.
    • Скидання стану замість вибіркового видалення — де можливо, повне пересоздання схеми/сідів між прогонами (у БД-тестах — відкат транзакції; механіка — у розділі про бази даних).

    Важливий нюанс залежно від ранера: у Playwright afterEach і teardown фікстури виконуються навіть після падіння тесту — це рятує від першої дірки. Але від смерті воркера чи помилки в самому teardown не рятує ніщо на рівні хуків, тому зовнішня страховка (TTL/клінап) потрібна завжди, коли тести пишуть у спільне сховище.

    Спільні оточення

    Ідеальний світ — ізольована база на кожен прогін (докеризована, testcontainers). Реальність частіше така: один staging, спільний на кілька команд, автотести, ручних тестувальників і демо для замовника. Це джерело болю, яке не лікується кодом тесту.

    Що ламається на спільному стенді: чужі дані (хтось залив 200 замовлень, і ваш «перевір, що список порожній» червоніє), гонки за спільний ресурс (два прогони деруться за єдиний тестовий акаунт — черга й флак), руйнування стану під ногами (ручний тестувальник видалив користувача, на якому тримався ваш сценарій), дрейф конфігурації (на стенді ввімкнули фіча-флаг, і поведінка змінилась).

    Пом'якшення — все та ж ізоляція, тільки на рівні даних, а не інфраструктури: не покладайтеся на глобальний стан. Тест не має вважати, що список порожній, — він створює свій маркований запис і шукає саме його. Не «є рівно 3 користувачі», а «серед користувачів є той, кого я щойно створив», з унікальним префіксом, щоб фільтрувати своє серед чужого. По суті, тест на спільному стенді має бути написаний так, ніби він там не один, — бо він там не один.

    Прод-дані: чому ні

    Спокуса зрозуміла: «візьмімо дамп продакшену — і дані будуть реалістичні». Це погана ідея з кількох незалежних причин, кожної з яких достатньо.

    Юридичний і приватний ризик. Дамп прода містить персональні дані (personally identifiable information, PII) реальних людей: імена, email, телефони, адреси, іноді платіжки. Копія в тестовій базі, в артефактах CI, у логах, на ноутбуці інженера — це обробка персональних даних поза призначенням і потенційний витік. GDPR (Регламент ЄС 2016/679) вимагає мінімізації даних і законної підстави; тестування продакшн-дампом такої підстави зазвичай не має.

    Непередбачуваність і старіння. Прод живий: користувача, на якому тримався тест, видалили; підписку скасували; баланс змінився платіжкою вночі. Тест на таких даних недетермінований за побудовою — він залежить від стану, який ви не контролюєте.

    Ризик мутації. Найгірший сценарій — тест не на копії, а на самому проді: автоматизація, що «перевіряє видалення», зносить дані справжнього клієнта. Прод — не тестовий полігон для сценаріїв, що змінюють стан.

    Що робити натомість: потрібне «як прод», а не «прод». Реалістичну структуру дають синтетичні дані з faker і фабрик або анонімізований (деперсоналізований) зріз — коли з прод-дампу PII замінюють на фейкові з збереженням форми й обсягу (маскування). Тоді ви маєте реалістичний розподіл і крайові випадки без чужих персональних даних. І окремо: якщо тестуєте саме на проді свідомо (post-deploy smoke), він має бути read-only або працювати на спеціально виділених синтетичних акаунтах, а не на живих користувачах.

    Типові помилки

    • Виглядає як флакі-тест, який «іноді падає в CI, а локально зелений», а насправді два воркери деруться за той самий email/акаунт — колізія унікального ключа під паралеллю. Лікується ізоляцією на воркер, а не ретраями.
    • Виглядає як стабільний зелений тест, а насправді він asserts на дані, які створив попередній тест у сьюті. Запустіть його окремо або зашардуйте — і він упаде. Приховане звʼязування через порядок.
    • Виглядає як «afterEach усе прибере», а насправді тест упав до кінця arrange або teardown сам кинув помилку — база тече. Потрібна TTL-страховка поверх хуків.
    • Виглядає як унікальний email від faker.internet.email(), а насправді випадковість не гарантує унікальності — на великій сьюті збіг настане. Унікальність домішують явно (індекс воркера, UUID, час).
    • Виглядає як «реалістичні дані з прод-дампу», а насправді PII в репозиторії/логах, юридичний ризик і половина записів протухла. Треба синтетика або анонімізований зріз.
    • Виглядає як швидка й зручна підготовка через прямий INSERT, а насправді тест зелений на стані, який застосунок ніколи не породив би, бо ви обійшли валідацію й тригери.

    Підсумок

    1. Кожен тест володіє своїми даними. Arrange створює те, що перевіряє; тест не залежить від порядку, іншого тесту чи глобального стану стенду.
    2. Готуй стан на найнижчому достатньому рівні. UI — лише коли перевіряєш сам UI-флоу; API — за замовчуванням; БД/бекдор — тільки коли стан інакше недосяжний, і свідомо.
    3. Дефолти — з фабрики, унікальність — з генератора. У тест виноси лише поле, що впливає на перевірку; крайові значення закладай явно, бо faker їх не дасть.
    4. Ізоляція під паралель + страховка на прибирання. Дані не колідують між воркерами (унікальний ключ або акаунт на воркер); after-хуки чистять, а TTL/клінап підмітає сиріт.
    5. Прод-дані в тестах — ні. PII, юридичний ризик, непередбачуваність і мутація живих даних. Потрібне «як прод» (синтетика, анонімізований зріз), а не «прод».

    Що питають на співбесіді

    • «Звідки ваші тести беруть дані?» Інтервʼюер перевіряє, чи ви взагалі думали про стратегію, а не хардкодите email у кожному тесті. Сильна відповідь розрізняє довідкові (статичні) й транзакційні (генеровані) дані й пояснює, чому.
    • «Тест створює замовлення для перевірки чекауту. Як приготуєте його?» Класика на розуміння сходів UI/API/БД. Очікують «через API, не кліками» з поясненням чому (швидкість, стабільність, поважання інваріантів) і коли виправданий виняток (перевіряємо сам флоу створення).
    • «Тест зелений локально, червоний у CI — з боку даних, що підозрюєте?» Дивляться на розуміння паралелізму: колізії між воркерами, залежність від порядку, спільний стенд, невідтворюваний faker без сида.
    • «Як прибираєте дані після тестів?» Слабка відповідь — «afterEach». Сильна — двошарова: хук плюс TTL/клінап, бо хук не виконається при смерті воркера чи падінні в arrange.
    • «Можна тестувати на копії продакшену?» Тут перевіряють зрілість: PII і GDPR, старіння даних, ризик мутації; альтернатива — синтетика або анонімізований зріз. Не побачити приватного ризику — тривожний сигнал.
    • «Фабрика проти статичної фікстури — коли що?» Дивляться, чи розумієте компроміс детермінованість/старіння/колізії, а не завчили одну відповідь.

    Джерела