Тест-дані: фабрики, фікстури, ізоляція
Зміст
Автотест — це не лише кроки й перевірки. Перш ніж клікнути «Оплатити», хтось має створити користувача, товар і кошик із грошима на балансі. Це «щось» і є тестові дані (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. Тест сам клікає по формах, щоб створити передумову. Єдиний випадок, коли це виправдано, — коли сам процес створення і є обʼєктом перевірки. У всіх інших випадках готувати стан кліками — марнотратство: повільно, крихко (кожна зміна форми ламає чужі тести) і змішує підготовку з перевіркою.
Через 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) — хуки взагалі не виконались.
Тому надійна стратегія двошарова: 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, а насправді тест зелений на стані, який застосунок ніколи не породив би, бо ви обійшли валідацію й тригери.
Підсумок
- Кожен тест володіє своїми даними. Arrange створює те, що перевіряє; тест не залежить від порядку, іншого тесту чи глобального стану стенду.
- Готуй стан на найнижчому достатньому рівні. UI — лише коли перевіряєш сам UI-флоу; API — за замовчуванням; БД/бекдор — тільки коли стан інакше недосяжний, і свідомо.
- Дефолти — з фабрики, унікальність — з генератора. У тест виноси лише поле, що впливає на перевірку; крайові значення закладай явно, бо faker їх не дасть.
- Ізоляція під паралель + страховка на прибирання. Дані не колідують між воркерами (унікальний ключ або акаунт на воркер); after-хуки чистять, а TTL/клінап підмітає сиріт.
- Прод-дані в тестах — ні. PII, юридичний ризик, непередбачуваність і мутація живих даних. Потрібне «як прод» (синтетика, анонімізований зріз), а не «прод».
Що питають на співбесіді
- «Звідки ваші тести беруть дані?» Інтервʼюер перевіряє, чи ви взагалі думали про стратегію, а не хардкодите email у кожному тесті. Сильна відповідь розрізняє довідкові (статичні) й транзакційні (генеровані) дані й пояснює, чому.
- «Тест створює замовлення для перевірки чекауту. Як приготуєте його?» Класика на розуміння сходів UI/API/БД. Очікують «через API, не кліками» з поясненням чому (швидкість, стабільність, поважання інваріантів) і коли виправданий виняток (перевіряємо сам флоу створення).
- «Тест зелений локально, червоний у CI — з боку даних, що підозрюєте?» Дивляться на розуміння паралелізму: колізії між воркерами, залежність від порядку, спільний стенд, невідтворюваний faker без сида.
- «Як прибираєте дані після тестів?» Слабка відповідь — «afterEach». Сильна — двошарова: хук плюс TTL/клінап, бо хук не виконається при смерті воркера чи падінні в arrange.
- «Можна тестувати на копії продакшену?» Тут перевіряють зрілість: PII і GDPR, старіння даних, ризик мутації; альтернатива — синтетика або анонімізований зріз. Не побачити приватного ризику — тривожний сигнал.
- «Фабрика проти статичної фікстури — коли що?» Дивляться, чи розумієте компроміс детермінованість/старіння/колізії, а не завчили одну відповідь.
Джерела
- ISTQB Certified Tester Foundation Level (CTFL) 4.0 Syllabus — тестові дані як частина активностей тестового процесу (розділ про імплементацію тестів).
- ISTQB Glossary — канонічні означення термінів test data, test fixture, test environment.
- Playwright: Fixtures — механіка фікстур, scope
testпротиworker, підготовка й прибирання стану. - @faker-js/faker — офіційна документація — генерація даних,
faker.seed()для відтворюваності. - Регламент ЄС 2016/679 (GDPR), ст. 5 — принципи мінімізації даних і законності обробки, що стоять за забороною прод-даних у тестах.
Що таке тестові дані і чому їх називають прихованим джерелом флаку?
Тестові дані (test data) — це все, що має існувати в системі до того, як тест почне клікати чи слати запити: користувач, товар, баланс, замовлення в потрібному статусі. Разом із ними йде поняття стану (state) — конкретна ситуація застосунку на момент перевірки. Небезпека в тому, що дані не видно в кроках сценарію: у звіті ви бачите «клік на кнопку», а не «взяли протухлий акаунт із чужого прогону». Через це тест червоніє там, де продукт справний, або, навпаки, зеленіє над реальною регресією. Саме тому стратегія даних відрізняє того, хто просто пише тести, від того, хто пише стабільні тести.
Чим фікстура відрізняється від стану і передумови?
Це три різні речі, які часто змішують. Фікстура (fixture) — фіксоване відоме оточення для тесту: заготовлений набір даних, залогінений юзер, піднятий сервіс; у сучасних ранерах цим словом називають і сам механізм, що готує оточення й прибирає його. Стан (state) — це конкретна ситуація в системі, наприклад «існує замовлення в статусі paid». Передумова (precondition) — те, що мусить бути істинним ще до першого кроку тесту. Практично межа проста: фікстура — це інструмент, стан — результат, якого вона досягає, а передумова — вимога, яку цей стан задовольняє.
Чому arrange-фазу автотесту вважають підготовкою даних?
Бо саме в arrange тест приводить систему до потрібного стану, а це і є створення та налаштування даних. Питання ніколи не стоїть як «готувати чи ні» — без передумов перевіряти нема що. Реальні питання інші: звідки взяти ці дані (файл, генератор, API, БД) і як не дати їм протекти в наступний тест. Тому дизайн arrange — це насправді дизайн стратегії тест-даних, а не просто пара рядків перед expect.
Коли статична фікстура доречніша за генерацію, а коли навпаки?
Статична фікстура — це заготовлені значення у файлі чи дампі; її сила у детермінованості й читабельності: видно рівно те, на чому тест зелений. Її слабкість — колізії та тихе старіння: захардкоджений email ламається, щойно два тести або воркери спробують створити його одночасно, а дамп із «активною підпискою» через місяць може стати «простроченим» після зміни правил бекенду. Генерація на льоту прибирає колізії й старіння, але коштує детермінованості: важче зрозуміти, на яких саме даних тест упав. Правильна відповідь — розподіл ролей, а не вибір одного. Довідкові стабільні дані (список країн, тарифи, конфіг фіч) тримають статичними, а транзакційні унікальні сутності (юзери, замовлення) генерують. Крайові значення на кшталт порожнього рядка чи 4-байтного unicode закладають явно, бо генератор їх випадково не видасть.
Що таке фабрика тест-даних і навіщо їй дефолти з можливістю перевизначення?
Фабрика (factory) — це функція-конструктор: вона віддає повністю заповнений обʼєкт з осмисленими значеннями за замовчуванням, а тобі лишає перевизначити тільки ті поля, що суттєві для конкретної перевірки. Це родич патернів Builder і Object Mother. Головна цінність не в «менше коду», а в читабельності за наміром: коли тест викликає buildUser({ role: 'admin' }), зрозуміло, що на результат впливає лише роль, а решта полів — випадковий фон. Виписуй той самий обʼєкт цілком, з усіма десятьма полями руками, — і вже не відрізниш, де значення принципове для перевірки, а де взяте абияк. Значення для полів фабрика зазвичай бере з генератора реалістичних даних.
Навіщо потрібен faker, якщо можна підставити рядок «test test»?
Faker — бібліотека для генерації правдоподібних фейкових значень: людські імена, поштові адреси, email, номери телефонів, UUID. Ця правдоподібність тут не косметика: ідеальне test test бреше про те, як виглядатимуть справжні значення, і ховає цілий клас багів. Довге прізвище з апострофом, нелатинські літери, адреса з незвичним форматом ловлять поламану верстку й помилки кодування, яких синтетично «чистий» рядок ніколи не покаже. Тобто faker не просто економить фантазію — він наближає тестові дані до бойових і робить їх ефективнішими на баг.
Чи гарантує faker унікальність значень, і чому це питання зі співбесіди?
Ні, і саме на цьому ловлять кандидатів. faker.internet.email() продукує випадкове, а не гарантовано унікальне значення: один і той самий рядок цілком може випасти вдруге, а на великій сьюті колізія настане неминуче — тут спрацьовує парадокс днів народження. Якщо покластися на «навряд чи збіжиться» для поля з унікальним ключем, рано чи пізно отримаєте 409 Conflict. Тому до таких ключів додають щось свідомо унікальне — таймстемп, номер воркера чи UUID. Faker відповідає за реалістичність, а за унікальність — окремий детермінований хвостик.
Навіщо сідувати faker і що це дає в CI?
Випадковість faker має зворотний бік — недетермінованість: кожен прогін дає інші значення, і відтворити червоний тест локально важко. Виклик faker.seed(123) фіксує стартову точку генератора, тож послідовність значень стає однаковою від прогону до прогону. Практичний прийом: у CI логувати використаний сид, і коли тест впав, повторити рівно ту саму послідовність даних на своїй машині. Це компроміс, що дає різноманіття генерації й водночас відтворюваність фікстури.
Якими чотирма способами можна довести систему до потрібного стану і як обрати між ними?
Стан на кшталт «замовлення в статусі paid» готують через UI, через API, прямим записом у БД або через бекдор. Через UI — коли обʼєкт перевірки і є сам процес створення; у решті випадків це повільно й крихко. Через API — золотий стандарт: швидко, стабільно і, головне, працює крізь ту саму бізнес-логіку, якою користується жива людина, тож неможливих станів не породжує. Пряма БД — найшвидший шлях і єдиний, коли стан недосяжний ззовні (наприклад, треба «зістарити» запис), але обходить усі інваріанти застосунку. Бекдор — спеціальний тестовий ендпоінт як компроміс між API і БД. Правило вибору одне: готуй стан на найнижчому достатньому рівні — API за замовчуванням, а БД і бекдор лише свідомо, коли інакше ніяк.
Чому підготовку стану через API вважають золотим стандартом arrange-фази?
Тому що API виграє одразу за трьома осями. Швидкість: HTTP-запит створює передумову на порядок швидше, ніж клікання по формах. Стабільність: публічний API міняється рідше, ніж верстка, тож підготовка не ламається від кожного редизайну кнопки. І найважливіше — коректність: запит проходить через ту саму валідацію й бізнес-правила, що й дія реального користувача, тому й не народжує ситуацій, яких у продакшені бути не може. Вхід так само проганяють запитом раз на весь ран і далі перевикористовують збережену сесію, а не заповнюють форму логіну перед кожним тестом.
Які ризики у прямого запису в БД і в бекдора, і коли вони все ж виправдані?
Пряма БД дає швидкість ціною коректності: INSERT обходить валідацію, тригери й обчислювані поля, тож на світ зʼявляється стан, до якого реальний код системи ніколи б не дійшов — і тест зеленіє на неможливих даних. Додатковий мінус — жорстке звʼязування зі схемою: міграція таблиці ламає підготовку. Бекдор мʼякший (це контрольований тестовий ендпоінт, а не сирий запит), але несе власну загрозу: якщо він доїде до продакшену, це готова вразливість — обхід автентифікації чи створення адмінів. Обидва інструменти тримають як виняток: коли стан справді недосяжний через API, і з ясним розумінням, що ви обходите логіку продукту.
Чому бекдор-ендпоінт не повинен існувати в продакшені?
Бекдор (backdoor) — це ендпоінт, що дозволяє одним запитом привести систему в потрібний стан в обхід звичайних перевірок, наприклад POST /test/seed-order. Саме те, що робить його зручним у тестах, робить його небезпечним у проді: ендпоінт, який створює сутності без автентифікації й валідації, — це пряма діра в безпеці, через яку сторонній може наробити адмінів чи підробити дані. Тому бекдор вмикають виключно за прапорцем середовища й пильнують, щоб у продакшен-конфігурації він був недоступний. Правило коротке: тестовий бекдор існує лише в тестових середовищах.
Чим ізоляція на тест відрізняється від ізоляції на воркер?
Ізоляція на тест означає, що arrange кожного тесту сам заводить потрібні дані й нічого не успадковує від сусіда попереду — це просто інша грань незалежності тестів. Її порушення легко впізнати: сценарій зелений у складі всієї сьюти, але червоніє, щойно запускаєш його наодинці — насправді він підхопив дані сусіда. Ізоляція на воркер зʼявляється під паралеллю: коли тести йдуть у кількох процесах, їхні дані не мають колідувати між процесами, інакше два воркери, що реєструють один email, дадуть 409 на другому. Розвʼязань два: додати до даних ознаку воркера, щоб вони не збігалися, або виділити кожному воркеру окрему пісочницю — свій акаунт, tenant чи простір імен. Перший рівень захищає від порядку й шардингу, другий — від паралельних процесів.
Тест зелений у повній сьюті, але червоний, коли запустити його окремо. Що це і в чому причина?
Це класичний симптом прихованого звʼязування через дані: тест мовчки покладається на стан, який створив хтось інший у сьюті. Наприклад, тест B робить assert на користувача, якого насправді завів тест A, тому наодинці B падає — потрібного юзера просто немає. Та сама поломка вилазить, якщо змінити порядок тестів або зашардувати сьюту між машинами. Лікування — не ретраї, а ізоляція на тест: кожен тест сам створює в arrange усе, що перевіряє, і нічого не очікує від сусідів. Якщо тест не виживає наодинці, він не незалежний, хоч би скільки разів був зеленим у наборі.
Як надійно прибирати тестові дані і чому «afterEach усе прибере» недостатньо?
Створені дані треба видаляти, бо інакше тестове сховище розбухає, простір унікальних ключів вичерпується, а майбутні прогони чіпляються за недоприбране. Стартовий інструмент — teardown у хуках (afterEach, afterAll) чи у фікстурі після use, але у нього є три дірки: тест упав посеред arrange і хук чистить за неповним списком; сам teardown кинув помилку; або воркер помер по таймауту чи OOM і хуки взагалі не виконались. Тому надійне рішення тримають на двох шарах: хук прибирає в штатному режимі, а поверх нього працює запобіжник, якому байдуже, чим скінчився тест. Роль такого запобіжника грає TTL (тестові сутності самі протухають і їх зносить фоновий джоб), нічний клінап за домовленою ознакою, одноразові акаунти чи повне скидання стану. У Playwright teardown виконується навіть після падіння тесту, що закриває першу дірку, але від смерті воркера рятує тільки зовнішня страховка.
Що таке TTL і одноразові акаунти як страховка прибирання?
TTL (time to live) — обмежений час життя тестової сутності: вона позначається так, що фоновий джоб (чи сам застосунок) видаляє її після закінчення строку, тож осиротілі дані зникають самі, навіть якщо хук не спрацював. Одноразові акаунти — інший підхід до тієї ж проблеми: замість чистити стан, тест щоразу створює свіжий акаунт і не переймається його вмістом, а прибирає їх той самий TTL-джоб. Обидва прийоми знімають залежність прибирання від успіху конкретного тесту чи виживання воркера. Ключова ідея — не намагатися видалити рівно те, що створив, а забезпечити, щоб усе тестове врешті зникло за загальною ознакою.
Як писати тести під спільний staging, який ділять кілька команд?
Спільний стенд підводить чотирма шляхами: чуже наповнення (хтось залив дві сотні замовлень — і перевірка «список має бути порожній» одразу червона), боротьба за єдиний ресурс (два прогони водночас хапають один акаунт — звідси черга й флак), ламання стану прямо під час прогону (ручний тестувальник зніс юзера, на якому тримався сценарій) і дрейф конфігурації (ввімкнули фіча-флаг — і поведінка попливла). Кодом тесту інфраструктуру не полагодити, тому рятує ізоляція на рівні даних: не спиратися на глобальний стан стенду. Замість припущення про порожній список тест сам заводить помічений власним префіксом запис і шукає рівно його серед чужих даних. Простими словами: писати тест так, ніби він на стенді не один, бо він там справді не один.
Чому не можна ганяти тести на дампі або копії продакшену?
Причин кілька, і кожної окремо достатньо. Юридична й приватна: у дампі лежать персональні дані (personally identifiable information, PII) справжніх користувачів — їхні імена, пошта, номери телефонів, подеколи платіжна інформація; щойно ця копія осідає в тестовій базі, логах чи на ноутбуці інженера, це вже обробка поза призначенням і потенційний витік, а GDPR наполягає на мінімізації даних і законній підставі, якої для такого тестування зазвичай бракує. Непередбачуваність: прод живий, і сутність, на якій тримався тест, можуть видалити чи змінити вночі платіжкою — тест стає недетермінованим за побудовою. Ризик мутації: найгірший варіант — тест не на копії, а на самому проді, і автоматизація «перевірки видалення» зносить дані реального клієнта. Потрібне «як прод», а не «прод»: синтетичні дані з faker і фабрик або анонімізований зріз, де PII замінено на фейкові зі збереженням форми й обсягу.
Що таке анонімізований зріз і чим він кращий за сирий прод-дамп?
Анонімізований (деперсоналізований) зріз — це копія структури прод-даних, у якій персональні поля замінено на фейкові з збереженням форми, розподілу й обсягу; цей процес називають маскуванням. Він дає те, заради чого взагалі тягнуться до продакшену, — реалістичний розподіл значень і крайові випадки, — але без чужих персональних даних у вашій базі, логах чи артефактах CI. На відміну від сирого дампа, тут немає юридичного ризику PII, і немає живих записів, які хтось змінить під ногами. Альтернатива тому самому — повністю синтетичні дані з фабрик і faker; вибір між ними залежить від того, наскільки важливо відтворити саме продакшн-розподіл, а не лише правдоподібну структуру.
Три кейси, де стратегія даних вирішує, зелений тест чи флак: фабрика з faker і гарантованою унікальністю, вибір рівня підготовки стану для чекауту, та ізоляція на воркер із двошаровим прибиранням. Скрізь — що робимо і чому саме так.
Кейс 1. Фабрика юзера: дефолти з faker, унікальність окремим хвостиком
Тесту потрібен адмін із балансом. Наївний варіант — тягнути повний обʼєкт із десятьма полями в кожен тест, і читач не розуміє, що з них суттєве. Фабрика прибирає цей шум: дефолти беремо з faker, а в тест виносимо тільки те, що впливає на перевірку.
import { faker } from '@faker-js/faker';
type User = { email: string; name: string; role: 'admin' | 'member'; balance: number };
// унікальний хвостик: faker дає реалістичність, а не унікальність
function uniqueEmail(): string {
return `qa+${Date.now()}-${faker.string.alphanumeric(6)}@test.local`;
}
function buildUser(overrides: Partial<User> = {}): User {
return {
email: uniqueEmail(),
name: faker.person.fullName(),
role: 'member',
balance: 0,
...overrides,
};
}
// у тесті видно намір: суттєві лише роль і баланс
const admin = buildUser({ role: 'admin', balance: 1000 });
Що дивитися і чому:
- Унікальність не довіряємо faker.
faker.internet.email()може повторитися, і на великій сьюті повториться напевно — це409 Conflictна другому тесті. Тому email збираємо з мітки часу й випадкового суфікса, а не покладаємось на «навряд чи збіжиться». ...overridesіде останнім. Спред у кінці дозволяє тесту перекрити будь-який дефолт; постав його на початок — і дефолти затруть значення, які передав тест.- Реалістичне імʼя ловить баги.
faker.person.fullName()дасть прізвище з апострофом чи нелатиницею, на яких провалиться крива верстка чи кодування — те, щоtest testзаховало б. - Сид для відтворення. Якщо тест впав на дивному згенерованому імені,
faker.seed(N)на старті ран повторить ту саму послідовність локально; сид варто логувати в CI.
Кейс 2. Той самий стан — чотири рівні. Що обрати для чекауту
Тесту чекауту потрібне замовлення в статусі paid. Спокуса зробити його кліками сильна, але дорога. Порівняння рівнів підготовки:
| Рівень | Швидкість | Стабільність | Поважає інваріанти | Коли обирати |
|---|---|---|---|---|
| UI | Найповільніше | Крихко | Так | Лише коли перевіряю сам флоу створення |
| API | Швидко | Висока | Так | За замовчуванням |
| Пряма БД | Найшвидше | Ламка на міграції | Ні, обходить логіку | Стан недосяжний ззовні (наприклад, «зістарити» запис) |
| Бекдор | Швидко | Середня | Частково | Компроміс, коли API незручний; тільки не в проді |
Для чекауту обʼєкт перевірки — оплата, а не створення замовлення, тож готуємо передумову через API:
import { test, expect } from '@playwright/test';
test('чекаут оплаченого замовлення', async ({ page, request }) => {
// arrange: стан готуємо запитом, не кліками — швидко й крізь ту саму бізнес-логіку
const res = await request.post('/api/orders', {
data: { items: [{ sku: 'BOOK-1', qty: 1 }], status: 'paid' },
});
expect(res.status()).toBe(201);
const order = await res.json();
// act + assert: перевіряємо саме те, заради чого написаний тест
await page.goto(`/orders/${order.id}`);
await expect(page.getByTestId('order-status')).toHaveText('Оплачено');
});
Що дивитися і чому:
- UI лишаємо для одного випадку. Готувати замовлення кліками виправдано, тільки якщо перевіряємо сам процес оформлення; інакше це повільно, крихко й змішує підготовку з перевіркою.
- API поважає інваріанти. Запит проходить крізь валідацію застосунку, тож не створить стану, неможливого в проді. Прямий
INSERTтого ж замовлення міг би поставитиpaidбез запису про оплату — тест зеленів би на неможливих даних. - Пряма БД — для недосяжного. Її беруть, коли стан ззовні не зробити, наприклад треба замовлення з датою місячної давнини для тесту на прострочення. Ціна — жорстке звʼязування зі схемою: міграція таблиці ламає підготовку.
- Бекдор лишається за прапорцем середовища. Ендпоінт
POST /test/seed-orderзручний, але в продакшені це діра в безпеці — його вмикають лише в тестових середовищах.
Кейс 3. Ізоляція на воркер плюс страховка на прибирання
Симптом «іноді падає в CI, локально зелено» часто означає, що два воркери деруться за той самий акаунт. Даємо кожному воркеру власний акаунт через worker-scoped фікстуру й підстраховуємо прибирання, бо хук не виживе, якщо воркер помре.
import { test as base, expect } from '@playwright/test';
type Account = { id: string; email: string };
export const test = base.extend<{}, { account: Account }>({
account: [async ({}, use, workerInfo) => {
// власний акаунт на воркер: створюється раз, живе весь прогін воркера
const email = `w${workerInfo.workerIndex}-${Date.now()}@test.local`;
const account = await createAccount(email);
await use(account); // усі тести цього воркера працюють у ньому
await deleteAccount(account.id); // основний прибиральник по завершенні воркера
}, { scope: 'worker' }],
});
test('замовлення видно лише своєму акаунту', async ({ account }) => {
const order = await createOrder(account.id, { sku: 'BOOK-1' });
// шукаємо саме СВІЙ запис — не «список порожній» і не «рівно один»
const list = await listOrders(account.id);
expect(list.map((o) => o.id)).toContain(order.id);
});
Що дивитися і чому:
- Ключ воркера прибирає колізії між процесами.
workerInfo.workerIndexу email гарантує, що два паралельні воркери не полізуть в один акаунт — саме та колізія, що дає плаваючі409у CI. Ретраї її не лікують, ізоляція лікує. - Спільний акаунт у межах воркера — прихована пастка. Тести одного воркера знову звʼязані: якщо вони ділять один кошик, то заважають одне одному. Тому кожен створює власне замовлення в межах акаунта, а не мутує спільний стан.
- Хук — лише перший шар.
deleteAccountпісляuseприбирає в нормі, але не спрацює при OOM чи таймауті воркера. Тому email містить домовлену ознаку (@test.local), за якою нічний клінап або TTL-джоб знесе сиріт незалежно від долі тесту. - Асерт шукає своє серед чужого.
toContain(order.id)виживе на спільному стенді, де є дані інших команд; асерт «рівно 1 замовлення» червонів би від першого чужого запису.
Словник і стратегія
- Розрізняю фікстуру (fixture), стан (state) і передумову (precondition): інструмент, досягнута ситуація і вимога до неї.
- Розумію, що arrange-фаза автотесту зводиться до готування даних і стану, і питання стоїть не «чи готувати», а «звідки брати» й «як не дати протекти».
Статичні фікстури проти генерації
- Знаю сильні сторони статичної фікстури (детермінованість, читабельність) і слабкі (колізії під паралель, тихе старіння даних).
- Розумію компроміс генерації: свіжі й безколізійні дані ціною детермінованості й ризику проґавити edge-кейс.
- Знаю розподіл ролей: довідкові стабільні дані — статичні, транзакційні унікальні сутності — генеровані, крайові значення — закладені явно.
Фабрики і faker
- Можу пояснити, що фабрика (factory) — це дефолти плюс перевизначення, і її головна цінність — читабельність за наміром, а не «менше коду».
- Знаю пастку:
faker.internet.email()не гарантує унікальності, тож до унікальних ключів домішую мітку часу, індекс воркера або UUID. - Можу пояснити, навіщо сідувати faker (
faker.seed()) — щоб відтворити червоний прогін за тією самою послідовністю даних.
Підготовка стану
- Знаю чотири рівні підготовки стану — UI, API, пряма БД, бекдор — і компроміси кожного.
- Тримаю в голові правило найнижчого достатнього рівня: кліки — тільки для перевірки самого UI-флоу, типовий шлях — API (швидкість, стабільність, поважання інваріантів), а БД чи бекдор беру свідомо, як виняток.
- Розумію ризик прямого
INSERT: обходить валідацію й тригери, тож тест зелений на стані, який продукт ніколи не породив би.
Ізоляція
- Розрізняю ізоляцію на тест (кожен сам створює свої дані) та на воркер (дані не колідують між паралельними процесами).
- Можу пояснити симптом прихованого звʼязування: «проходить у сьюті, падає окремо» або залежить від порядку чи шардингу.
- Знаю два способи ізоляції на воркер: позначити дані ознакою воркера проти збігів або виділити кожному окрему пісочницю (акаунт, tenant).
Прибирання
- Знаю три дірки наївного teardown: падіння посеред arrange, помилка в самому teardown, смерть воркера.
- Можу пояснити двошарову стратегію: хук прибирає штатно, а зовнішній запобіжник спрацьовує незалежно від того, чим завершився тест.
- Знаю варіанти страховки: TTL, нічний клінап за ознакою, одноразові акаунти, повне скидання стану.
Спільні оточення і прод-дані
- Можу пояснити принцип «не покладайся на глобальний стан» на спільному стенді (чужі дані, гонки за ресурс, дрейф конфігурації): тест шукає власний маркований запис, а не «рівно 3 користувачі».
- Розумію три незалежні причини не брати прод-дані: PII і GDPR, непередбачуваність та старіння, ризик мутації живих записів.
- Знаю альтернативи «як прод, а не прод»: синтетичні дані з фабрик і faker або анонімізований (маскований) зріз.
Що таке фікстура (fixture) у контексті автотестів?
Питання
Фікстура, стан, передумова — у чому різниця?