Тестові дані: крайові значення, генерація, ізоляція
Зміст
Техніка тест-дизайну відповідає на питання «що перевірити», але кейс не запуститься, поки в ньому не зʼявиться конкретне значення: не «невалідний email», а іван@, не «межа довжини», а рядок рівно з 256 символів. Саме на цьому кроці губиться половина цінності дизайну: клас еквівалентності виділили правильно, а представником узяли test123 — і баг, який ловиться лише на апострофі чи емодзі, спокійно їде в прод.
Друга половина болю — не «які дані», а «що з ними відбувається між прогонами». Тест, що вчора був зелений, сьогодні падає з «user already exists», бо сам собі лишив сміття; згенерована дата раз на місяць влучає в 31 число — і сюїт «флакає» без видимої причини. Ця глава — про обидві половини: як від техніки дійти до конкретного значення, які значення небезпечні завжди, коли фіксувати набір, а коли генерувати, і як ізолювати дані, щоб прогін можна було повторювати нескінченно. Стратегія керування даними в автоматизації ширше — фабрики, сідинг бази, менеджмент середовищ — це окрема глава розділу «Автоматизація»; тут — кут тест-дизайну.
Дані — продовження технік, не додаток до них
Ланцюжок простий: техніка виділяє тестову умову, умова перетворюється на конкретне значення, значення лягає в кейс. Класи еквівалентності кажуть, скільки принципово різних значень потрібно (по одному представнику з класу), граничні значення — які саме точки взяти на стиках класів. Таблиці рішень додають комбінації: правило R3 «премія + прострочення» — це вже не одне значення, а узгоджений набір полів.
З цього випливає робоче означення: тестові дані — це представники класів і меж, матеріалізовані в конкретні байти. Якщо значення не представляє жодного класу і не сидить на жодній межі — воно нічого не покриває. qwerty у полі імені перевіряє рівно те саме, що Іван, тобто один і той самий валідний клас — другий кейс зайвий.
І ще одне розрізнення, яке економить години дебагу: дані бувають вхідні (те, що тест вводить: значення полів, тіло запиту) і дані стану (те, що має існувати до кроку: зареєстрований користувач, товар на складі, активна підписка). Техніки з попередніх глав здебільшого працюють із вхідними; проблеми повторюваності прогонів — майже завжди про дані стану. Далі розберемо обидва шари.
Небезпечна класика: рядки, які ламають усе
Є набір значень, які досвідчений QA підставляє в будь-яке текстове поле автоматично — це, по суті, готовий чеклист із досвідних технік, зібраний із десятиліть однакових багів:
| Категорія | Приклади | Що зазвичай ламається |
|---|---|---|
| Порожнє і пробіли | "", рядок лише з пробілів, " Іван " | валідація обовʼязковості, обрізання пробілів (trim), пошук і порівняння |
| Довжина | 1 символ, рівно максимум, максимум+1, 10 000 символів | німе обрізання в базі, розʼїхана верстка, ліміти API |
| Unicode | О'Браєн, Zoë, 😀, кирилиця + латиниця в одному слові | підрахунок довжини, нормалізація, пошук |
| Спецсимволи | ', ", <script>, %, _, перенос рядка | екранування, SQL-запити з LIKE, вивід без санітизації |
Чому саме ці — механіка, а не забобони.
Порожнє і пробіли. «Порожнє поле» і «поле з трьох пробілів» — різні класи: перше ловиться перевіркою на обовʼязковість, друге проходить її і падає далі — у пошуку, у листах, у звітах. Пробіли по краях (" Іван ") перевіряють, чи система робить trim і чи робить його однаково при записі й при порівнянні: класичний баг — реєстрація обрізає пробіли, а логін ні.
Довжина. Це чисті граничні значення: якщо ліміт 255 — потрібні 255 і 256. Але є і третя точка — «абсурдно довге»: тисячі символів перевіряють не валідацію, а те, що станеться, коли валідації немає: колонка VARCHAR(255) у поблажливому режимі СУБД мовчки обріже рядок, і користувач втратить половину тексту без жодної помилки.
Unicode. Найпідступніша категорія, бо «виглядає однаково — не означає однакове». Емодзі 😀 у JavaScript має length === 2, бо рядки рахуються в одиницях UTF-16, а не в символах — ліміт «до 10 символів» може відсікти 5 емодзі. Літера «é» існує у двох виглядах: один код-пойнт або «e» плюс комбінований акцент — без нормалізації (NFC/NFD) два візуально ідентичні імені не збігаються при порівнянні. Апостроф у О'Браєн чи Марʼяна — легальна частина імені, яка десятиліттями валить наївне екранування. Як ці символи виживають у запитах і формах — окрема історія, розібрана в главі URL і кодування.
Спецсимволи. ' і <script> перевіряють гігієну на вході (екранування SQL, санітизація HTML) — глибокий розбір цих атак належить розділу про безпеку, але базові значення в наборі даних мусять бути завжди. Менш відомі % і _ — символи-маски в SQL-операторі LIKE: пошук за рядком 100% без екранування знайде і «1000», і «100500».
Важливо: це не «список магічних рядків для заучування», а наслідок технік — кожен рядок сидить або на межі (довжина), або в окремому невалідному класі (порожнє, спецсимволи), або перевіряє неявне припущення коду (Unicode).
Дати, таймзони, float і локалі
Друга група небезпечної класики — не рядки, а типи зі складною внутрішньою механікою.
Дати. Межі тут неочевидні, бо календар нерівномірний: 29 лютого (високосний рік), 31 число (перехід у місяць, де його немає), 31 грудня → 1 січня (межа року і, часто, звітного періоду). Додаткові точки — дата народження в майбутньому, дати до 1970 року (початок епохи Unix) і переходи на літній/зимовий час там, де вони є: доба, у якій одна година зникає або трапляється двічі.
Таймзони. Класичний сценарій: сервер живе в UTC, користувач — у Києві; о 1:00 ночі за Києвом «сьогодні» на клієнті й на сервері — різні дати, і замовлення потрапляє «у вчорашній» звіт. Конвенція галузі — зберігати час в UTC і конвертувати в локальний лише при показі; тестові дані для дат мають включати моменти близько півночі саме тому, що вони ловлять порушення цієї конвенції.
Float. Двійкове представлення дробів (IEEE 754) не вміє точно зберігати десяткові: 0.1 + 0.2 у JavaScript дає 0.30000000000000004. Наслідок перший — грошові суми ніколи не тримають у float: або цілі копійки/центи, або десятковий тип. Наслідок другий — в автотестах дробові числа не порівнюють на строгу рівність:
// Падатиме епізодично залежно від шляху обчислення:
expect(total).toBe(0.3);
// Правильно — порівняння з точністю:
expect(total).toBeCloseTo(0.3, 2);
Локалі. Одні й ті самі дані виглядають по-різному: 1.5 проти 1,5 (десяткова крапка проти коми), 03/07/2026 — це 3 липня чи 7 березня залежно від формату країни. Поле, що парсить «число з комою», — окремий клас еквівалентності для кожної підтримуваної локалі; сортування списку імен із діакритиками — ще один. Мінімальний набір даних для інтернаціоналізованого продукту завжди містить значення хоча б із двох локалей із різними правилами.
Фіксовані набори чи генерація
Звідки беруться самі значення — три підходи з різною механікою.
Фіксований набір — значення прописані прямо в кейсі або таблиці параметризації. Головна перевага — детермінізм: кейс сьогодні і кейс через рік перевіряють байт у байт одне й те саме, падіння тривіально відтворюється. Головна вада — той самий детермінізм: набір старіє разом із кодом (парадокс пестицидів із принципів тестування, у CTFL 4.0 сформульований як «тести зношуються» — тест, що ганяє одні й ті самі значення, з часом перестає знаходити нові баги). Для меж і класів фіксований набір — правильний вибір: межа 255/256 не зміниться від прогону до прогону, їй нічого генерувати.
Генерація — коли потрібен обсяг реалістичних даних: сто користувачів з іменами, адресами й телефонами для тесту пагінації. Бібліотека faker (@faker-js/faker) генерує правдоподібні значення за доменами (імена, email, адреси), і критично важлива її можливість — сід (seed):
import { faker } from '@faker-js/faker';
faker.seed(20260716); // однаковий сід → однакова послідовність значень
const user = {
name: faker.person.fullName(),
email: faker.internet.email(),
};
Правило без винятків: випадковість без зафіксованого чи залогованого сіда — це вбудований у сюїт флак. Тест упав на згенерованому значенні, сід не записаний — падіння не відтворити, баг не завести.
Property-based тестування — третій підхід, який змінює саму постановку: замість «для входу X очікуємо Y» формулюється властивість (property) — інваріант, що має виконуватися для будь-якого входу, а фреймворк сам генерує сотні випадкових значень і шукає контрприклад. У TypeScript-стеку найпоширеніша бібліотека — fast-check:
import fc from 'fast-check';
// Інваріант: форматування суми і зворотний парсинг повертають вихідне число
fc.assert(
fc.property(fc.integer({ min: 0, max: 1_000_000_00 }), (kopiykas) => {
return parseAmount(formatAmount(kopiykas)) === kopiykas;
}),
);
Коли контрприклад знайдено, fast-check виконує звуження (shrinking) — автоматично мінімізує його до найпростішого значення, що все ще ламає властивість: замість «упало на 738 291» ви отримаєте «упало на 100» — і межу видно неозброєним оком. Для junior-рівня достатньо розуміти ідею і вміти назвати відмінність: приклад перевіряє точку, властивість — увесь простір входів.
Підходи не конкурують, а складаються: межі — фіксовано, масовка — генерацією, чиста логіка без побічних ефектів (парсери, форматери, розрахунки) — властивостями.
Ізоляція: щоб прогін можна було повторити
Тепер про дані стану. Симптоми поганої ізоляції впізнаються миттєво: тест зелений локально і червоний у CI; сюїт проходить цілком, але окремий тест сам по собі падає; другий прогін поспіль валиться на «duplicate entry». Причина одна — тести ділять дані: з попереднім прогоном, із сусіднім тестом, із живими користувачами staging-середовища.
Базовий принцип: тест сам створює те, що йому потрібно, і не покладається на чуже. Два інструменти, які це забезпечують:
Унікальність замість прибирання. Найдешевша ізоляція — щоразу нові дані: email із UUID не конфліктне ні з попереднім прогоном, ні з паралельним воркером. Прибирання (cleanup) корисне, але покладатися лише на нього не можна: тест, що впав посередині, до прибирання не дійде — і без унікальності наступний прогін успадкує сміття.
Створення через API у фікстурі. Дані стану готуються не кліками через UI (повільно й крихко), а прямим викликом API в setup-фазі. У Playwright природне місце для цього — фікстура: вона гарантує, що прибирання виконається навіть після падіння тесту:
import { test as base } from '@playwright/test';
export const test = base.extend({
freshUser: async ({ request }, use) => {
const email = `qa+${crypto.randomUUID()}@example.com`;
const res = await request.post('/api/users', { data: { email } });
const { id } = await res.json();
await use({ id, email }); // сам тест працює тут
await request.delete(`/api/users/${id}`); // teardown — навіть якщо тест упав
},
});
Домен example.com тут не випадковий: RFC 2606 резервує його (разом із доменами .test, .example, .invalid) саме для документації й тестів — лист на таку адресу гарантовано не поїде живій людині.
Рівнем вище ті самі ідеї масштабуються: окремий простір даних на кожен паралельний воркер, відкат транзакції після тесту, повний пересів (seeding) бази перед сюїтом. Це вже інженерія розділу «Автоматизація»; з погляду тест-дизайну важливий сам інваріант — кейс описує і свої вхідні дані, і передумови стану так, щоб їх можна було відтворити з нуля.
Анонімізація чутливих даних
Спокуса «просто злити копію проду в тестову базу» зрозуміла: дані реалістичні, обсяги справжні, зв'язки цілісні. Але прод-база — це персональні дані живих людей: імена, телефони, адреси, платіжні реквізити. Копія на тестовому стенді — це ті самі персональні дані з у рази слабшим захистом: ширші доступи, логи, дампи на ноутбуках. Для компаній під GDPR (Регламент ЄС 2016/679) і українським законодавством про захист персональних даних це не «неохайність», а порушення з реальною відповідальністю. Окремий побутовий ризик: тестовий прогін на копії проду розсилає справжні листи справжнім користувачам.
Три техніки знешкодження, від простішої до глибшої:
- Маскування (masking) — частину значення затирають:
380*****1234. Просто, але дані втрачають форму — замаскований телефон не пройде валідацію. - Псевдонімізація (pseudonymization) — реальні значення послідовно замінюються вигаданими: Олена Ковальчук у всіх таблицях стає, скажімо, Мартою Іванчук. Форма і звʼязки збережені, але GDPR прямо визначає: псевдонімізовані дані лишаються персональними, бо заміна оборотна за наявності ключа.
- Синтетичні дані — база генерується з нуля (той самий faker) зі збереженням розподілів і звʼязків. Єдиний варіант, коли на виході взагалі немає слідів реальних людей — і тому найбезпечніший за замовчуванням.
Ключова вимога до будь-якої заміни — зберегти властивості, від яких залежить поведінка системи: валідний формат телефону й email, реалістичні дати народження, консистентність між таблицями (один користувач → один псевдонім скрізь). Анонімізація, що ламає формат, знецінює тести тихо: валідація відсікає «зіпсовані» записи, і половина логіки просто не виконується.
Типові помилки
- Виглядає як флак «тест падає раз на кілька тижнів», а насправді згенерована дата періодично влучає в 31 число або кінець місяця — межа, яку ніхто не зафіксував окремим кейсом.
- Виглядає як ретельний набір даних «my name», «test test», «123», а насправді всі три — представники одного валідного класу: жодного порожнього, жодного апострофа, жодної межі довжини. Три кейси — покриття як від одного.
- Виглядає як надійна унікальність через
Date.now(), а насправді паралельні воркери отримують однаковий таймстемп у ту саму мілісекунду — і ловлять колізію. UUID не має цієї проблеми. - Виглядає як акуратність «кожен тест прибирає за собою в кінці», а насправді тест, що впав до кроку прибирання поза фікстурою/teardown, лишає сміття — і без унікальних даних наступний прогін отруєний.
- Виглядає як зручність «на тестовому стенді ж несправжні користувачі», а насправді стенд наповнений учорашнім дампом проду з реальними ПІБ і телефонами — і кожен, хто має доступ до стенда, має доступ до персональних даних.
Підсумок
- Тестові дані — матеріалізовані класи й межі: значення, яке не представляє клас і не сидить на межі, не покриває нічого.
- Небезпечна класика обовʼязкова в кожному текстовому полі: порожнє, пробіли, максимум довжини і максимум+1, апостроф, емодзі,
<script>. - Дати, таймзони, float і локалі — типи з нерівномірною механікою: 29 лютого, північ у чужій таймзоні,
0.1 + 0.2і десяткова кома — окремі кейси, а не екзотика. - Межі — фіксованими значеннями, масовка — генерацією з сідом, інваріанти чистої логіки — property-based; випадковість без залогованого сіда — вбудований флак.
- Тест сам створює свої дані, робить їх унікальними і не ділить їх ні з ким; прод-дані на тестовому стенді — інцидент безпеки, а не зручність.
Що питають на співбесіді
- «Які дані ви підготуєте для поля імені користувача?» — інтервʼюер чекає не потік значень, а систему: класи → межі → небезпечна класика (порожнє, пробіли, довжина, Unicode, апостроф). Хаотичний перелік без пояснення «навіщо кожне» — слабка відповідь. Повний розбір такої задачі — у главі Класичні задачі співбесід.
- «Тести падають при повторному прогоні, з першого — зелені. Де шукати?» — перевіряють розуміння ізоляції: спільні дані між прогонами, залежність від порядку, відсутність унікальності. Сильний кандидат називає і діагноз, і ліки (унікальні дані, створення через API, teardown у фікстурі).
- «Чим property-based тестування відрізняється від параметризованого тесту?» — достатньо суті: параметризація ганяє фіксований список прикладів, property-based перевіряє інваріант на згенерованому просторі входів і мінімізує контрприклад.
- «Можна взяти копію продакшн-бази для тестування?» — питання на зрілість: очікують «ні без знешкодження», різницю анонімізації й псевдонімізації та розуміння, що замінені дані мають зберігати формат і звʼязки.
- «Чому тест на суму замовлення інколи падає на копійку?» — перевірка базової грамотності: float і IEEE 754, гроші в цілих копійках, порівняння з точністю замість строгої рівності.
Джерела
- ISTQB CTFL 4.0 Syllabus — розділ 4.2 (техніки чорної скриньки) — фундамент вибору значень; підготовку тестових даних силабус відносить до активності тест-імплементації (розділ 1.4).
- ISTQB Glossary — офіційні означення test data, test data preparation, anonymization.
- fast-check: документація — property-based тестування для TypeScript: арбітрарії, властивості, shrinking.
- Faker: документація — генерація реалістичних даних і відтворюваність через seed.
- RFC 2606: Reserved Top Level DNS Names — зарезервовані домени
example.com,.test,.invalidдля безпечних тестових адрес.
Що таке тестові дані і як вони пов'язані з техніками тест-дизайну?
Тестові дані — це представники класів еквівалентності й граничних значень, матеріалізовані в конкретні байти: не «невалідний email», а іван@, не «межа довжини», а рядок рівно з 256 символів. Ланцюжок такий: техніка виділяє тестову умову, умова перетворюється на конкретне значення, значення лягає в кейс. Класи еквівалентності кажуть, скільки принципово різних значень потрібно, граничні значення — які саме точки взяти на стиках класів, таблиці рішень додають узгоджені набори полів для комбінацій. Практичний наслідок: якщо значення не представляє жодного класу і не сидить на жодній межі — воно нічого не покриває. qwerty у полі імені перевіряє рівно те саме, що Іван, — один валідний клас, тож другий кейс зайвий.
Чим вхідні дані відрізняються від даних стану?
Вхідні дані — те, що тест вводить: значення полів, тіло запиту. Дані стану — те, що має існувати до кроку: зареєстрований користувач, товар на складі, активна підписка. Розрізнення економить години дебагу, бо проблеми в них різні: техніки тест-дизайну здебільшого працюють із вхідними даними, а проблеми повторюваності прогонів — «зелений учора, червоний сьогодні» — майже завжди про дані стану. Коли тест падає з «user already exists», шукати треба не у виборі значень, а в тому, що лишилося в базі від попереднього прогону.
Які дані ви підготуєте для текстового поля, наприклад для імені користувача?
Інтерв'юер чекає не потік значень, а систему: спочатку класи (валідне ім'я, порожнє, задовге), потім межі (1 символ, рівно максимум, максимум+1), потім небезпечну класику. Це порожній рядок і рядок лише з пробілів, ім'я з пробілами по краях (" Іван "), апостроф (О'Браєн), Unicode (діакритики, емодзі, кирилиця + латиниця в одному слові), спецсимволи (', ", <script>) і «абсурдно довге» — тисячі символів. Кожне значення має пояснення «навіщо»: воно або сидить на межі, або представляє окремий невалідний клас, або перевіряє неявне припущення коду. Хаотичний перелік без пояснення — слабка відповідь.
Чому «порожнє поле» і «поле з трьох пробілів» — різні тестові випадки?
Це різні класи еквівалентності, бо їх обробляють різні шари коду. Порожнє поле ловиться перевіркою на обов'язковість, а рядок із пробілів цю перевірку проходить — і падає далі: у пошуку, у листах, у звітах. Окремо пробіли по краях (" Іван ") перевіряють, чи система робить trim і чи робить його однаково при записі й при порівнянні. Класичний баг цього класу: реєстрація обрізає пробіли, а логін — ні, і користувач не може зайти під власним іменем.
Навіщо перевіряти «абсурдно довгий» рядок, якщо межі ліміту вже покриті значеннями 255 і 256?
Значення 255 і 256 перевіряють валідацію — а тисячі символів перевіряють, що станеться, коли валідації немає. Типовий сценарій: колонка VARCHAR(255) мовчки обріже рядок, і користувач втратить половину тексту без жодної помилки — німе обрізання не зловить жоден кейс «на межі», бо система формально не відмовила. Додатково довгий рядок ловить розʼїхану верстку і ліміти API. Це третя точка категорії «довжина», яка перевіряє не правило, а його відсутність.
Чому емодзі й діакритики — обов'язкові значення для текстових полів?
Бо в Unicode «виглядає однаково» не означає «однакове», і код повний неявних припущень про це. Емодзі 😀 у JavaScript має length === 2, бо рядки рахуються в одиницях UTF-16, а не в символах — ліміт «до 10 символів» може відсікти 5 емодзі. Літера «é» існує у двох виглядах: один код-пойнт або «e» плюс комбінований акцент — без нормалізації (NFC/NFD) два візуально ідентичні імені не збігаються при порівнянні. Апостроф у О'Браєн чи Марʼяна — легальна частина імені, що десятиліттями валить наївне екранування. Практичний наслідок: підрахунок довжини, пошук і порівняння — три місця, де Unicode-дані ламають те, що на test123 виглядало ідеально.
Чим небезпечні символи % і _ у пошукових полях?
Це символи-маски в SQL-операторі LIKE: % — будь-яка послідовність символів, _ — рівно один символ. Якщо ввід користувача потрапляє в LIKE-запит без екранування, пошук за рядком 100% знайде і «1000», і «100500» — система поверне зайві результати без жодної помилки. На відміну від ' чи <script>, які знає більшість, маски LIKE — менш відома частина небезпечної класики, і саме тому їх часто пропускають. У наборі даних для будь-якого пошуку вони мають бути поруч із апострофом і тегами.
Які крайові значення ви візьмете для поля дати?
Межі дат неочевидні, бо календар нерівномірний. Обов'язкові точки: 29 лютого (високосний рік), 31 число (перехід у місяць, де його немає), 31 грудня → 1 січня (межа року і, часто, звітного періоду). Далі — залежно від домену: дата народження в майбутньому, дати до 1970 року (початок епохи Unix), доба переходу на літній/зимовий час, у якій одна година зникає або трапляється двічі. І окремо — моменти близько півночі, бо саме вони ловлять баги таймзон: о 1:00 ночі за Києвом «сьогодні» на клієнті й на сервері в UTC — різні дати, і замовлення їде «у вчорашній» звіт. Конвенція галузі — зберігати час в UTC і конвертувати в локальний лише при показі; значення біля півночі якраз і перевіряють, що її не порушено.
Чому тест на суму замовлення інколи падає на копійку?
Це float і IEEE 754: двійкове представлення дробів не вміє точно зберігати десяткові, тому 0.1 + 0.2 у JavaScript дає 0.30000000000000004. Падіння епізодичне, бо похибка залежить від шляху обчислення — звідси враження «флаку». Наслідків два. Перший — грошові суми ніколи не тримають у float: або цілі копійки/центи, або десятковий тип. Другий — в автотестах дробові числа не порівнюють на строгу рівність: замість expect(total).toBe(0.3) — порівняння з точністю expect(total).toBeCloseTo(0.3, 2).
Що дають тестам дані з різних локалей?
Одні й ті самі дані виглядають по-різному залежно від локалі: 1.5 проти 1,5 (десяткова крапка проти коми), 03/07/2026 — це 3 липня чи 7 березня залежно від формату країни. Поле, що парсить «число з комою», — окремий клас еквівалентності для кожної підтримуваної локалі; сортування списку імен із діакритиками — ще один. Практичне правило: мінімальний набір даних для інтернаціоналізованого продукту завжди містить значення хоча б із двох локалей із різними правилами. Інакше парсер, який чудово їсть крапку, тихо ламається на першому ж користувачеві з комою.
Коли тестові дані фіксувати в кейсі, а коли генерувати?
Критерій — що потрібно від даних. Межі й класи — фіксованими значеннями: межа 255/256 не зміниться від прогону до прогону, їй нічого генерувати, а детермінізм дає тривіальне відтворення падінь. Генерація (faker) — коли потрібен обсяг реалістичних даних: сто користувачів з іменами й адресами для тесту пагінації. Property-based (fast-check) — коли є інваріант, що має виконуватися на всьому просторі входів. Підходи не конкурують, а складаються: межі — фіксовано, масовка — генерацією, чиста логіка без побічних ефектів — властивостями. Вада фіксованих наборів — парадокс пестицидів: тест, що ганяє одні й ті самі значення, з часом перестає знаходити нові баги.
Навіщо генератору випадкових даних сід (seed)?
Сід робить випадковість відтворюваною: однаковий сід дає однакову послідовність значень, тож faker.seed(20260716) перетворює «випадковий» прогін на детермінований. Правило без винятків: випадковість без зафіксованого чи залогованого сіда — це вбудований у сюїт флак. Механіка провалу проста: тест упав на згенерованому значенні, сід не записаний — падіння не відтворити, баг не завести, і команда списує його на «мигнуло». Мінімальна дисципліна — логувати сід на початку прогону, тоді будь-яке падіння повторюється точним відтворенням послідовності.
Чим property-based тестування відрізняється від параметризованого тесту?
Параметризація ганяє фіксований список прикладів: «для входу X очікуємо Y» стільки разів, скільки рядків у таблиці. Property-based змінює постановку: формулюється властивість (property) — інваріант, що має виконуватися для будь-якого входу, наприклад «форматування суми і зворотний парсинг повертають вихідне число», — а фреймворк (у TypeScript-стеку — fast-check) сам генерує сотні випадкових значень і шукає контрприклад. Тобто приклад перевіряє точку, властивість — увесь простір входів. Підхід найкраще працює на чистій логіці без побічних ефектів: парсери, форматери, розрахунки.
Що таке звуження (shrinking) у property-based тестуванні?
Коли фреймворк знайшов контрприклад, він не зупиняється, а автоматично мінімізує його до найпростішого значення, що все ще ламає властивість. Замість «упало на 738 291» ви отримуєте «упало на 100» — і межу, на якій ховається баг, видно неозброєним оком. Без shrinking довелося б руками відрізати цифри від випадкового значення, щоб зрозуміти, у чому суть. Це і є практична цінність property-based понад «багато випадкових входів»: не просто знайти падіння, а віддати його в мінімальній формі, готовій для баг-репорту.
Тести падають при повторному прогоні, з першого — зелені. Де шукати?
Це класичний симптом поганої ізоляції даних стану: тести ділять дані з попереднім прогоном, сусіднім тестом чи живими користувачами стенда. Впізнавані прояви: «duplicate entry» або «user already exists» на другому прогоні поспіль, зелений локально — червоний у CI, сюїт проходить цілком, а окремий тест сам по собі падає. Діагноз — тест покладається на чужі або залишені дані; ліки — базовий принцип «тест сам створює те, що йому потрібно»: унікальні дані на кожен прогін (email з UUID), створення стану через API в setup-фазі, прибирання в teardown фікстури. Сильний кандидат називає і діагноз, і ліки.
Чому унікальність даних надійніша за прибирання (cleanup), і чим поганий Date.now() для унікальності?
Прибирання корисне, але покладатися лише на нього не можна: тест, що впав посередині, до кроку прибирання поза фікстурою не дійде — і наступний прогін успадкує сміття. Унікальність знімає саму проблему: email з UUID не конфліктує ні з попереднім прогоном, ні з паралельним воркером, тож навіть залишене сміття нічому не заважає. Date.now() виглядає як надійна унікальність, а насправді паралельні воркери отримують однаковий таймстемп у ту саму мілісекунду — і ловлять колізію; UUID цієї проблеми не має. Робоча комбінація: унікальність як гарантія плюс teardown у фікстурі як гігієна.
Чому дані стану готують через API у фікстурі, а не кліками через UI?
UI-підготовка повільна і крихка: кожен клік — потенційна точка відмови, яка не стосується того, що тест перевіряє. Прямий виклик API в setup-фазі створює той самий стан за мілісекунди й падає лише тоді, коли зламано сам API. У Playwright природне місце — фікстура: вона гарантує, що teardown виконається навіть після падіння тесту, чого не дає прибирання «в кінці тесту». Окрема деталь: тестові email беруть на домені example.com, бо RFC 2606 резервує його (разом із .test, .example, .invalid) для документації й тестів — лист на таку адресу гарантовано не поїде живій людині.
Можна взяти копію продакшн-бази для тестування?
Ні — без знешкодження це інцидент безпеки, а не зручність. Прод-база — персональні дані живих людей, а копія на тестовому стенді має в рази слабший захист: ширші доступи, логи, дампи на ноутбуках; для компаній під GDPR і українським законодавством про захист персональних даних це порушення з реальною відповідальністю. Побутовий ризик на додачу: тестовий прогін на копії проду розсилає справжні листи справжнім користувачам. Варіанти знешкодження — маскування (частину значення затирають), псевдонімізація (послідовна заміна на вигадані значення) і синтетичні дані, згенеровані з нуля; останній варіант найбезпечніший, бо на виході взагалі немає слідів реальних людей.
Чим псевдонімізація відрізняється від анонімізації, і чому це важливо для GDPR?
Псевдонімізація — послідовна заміна реальних значень вигаданими: Олена Ковальчук у всіх таблицях стає Мартою Іванчук, форма і зв'язки збережені. Але GDPR прямо визначає: псевдонімізовані дані лишаються персональними, бо заміна оборотна за наявності ключа — тобто юридично база «з псевдонімами» вимагає того самого захисту, що й прод. Справжня анонімізація — коли зворотного шляху немає; практично найчистіший спосіб — синтетичні дані, згенеровані з нуля зі збереженням розподілів і зв'язків. На співбесіді це питання на зрілість: очікують не «можна/не можна», а розуміння різниці між рівнями знешкодження.
Чому анонімізовані дані мають зберігати формат і зв'язки оригіналу?
Бо система поводиться з даними за їхніми властивостями: замаскований телефон 380*****1234 не пройде валідацію формату, і код, що йде після валідації, просто не виконається. Ключова вимога до будь-якої заміни — зберегти властивості, від яких залежить поведінка: валідний формат телефону й email, реалістичні дати народження, консистентність між таблицями (один користувач → один псевдонім скрізь). Анонімізація, що ламає формат, знецінює тести тихо: валідація відсікає «зіпсовані» записи, половина логіки не виконується, а покриття на папері виглядає повним. Тому вибір техніки — це баланс: маскування найпростіше, але руйнує форму; синтетика найдорожча, але дає і безпеку, і робочі властивості.
Три кейси з робочого життя: перетворення технік на конкретний набір значень для одного поля (з параметризованим Playwright-тестом), розбір флаку «падає при повторному прогоні» з лікуванням через фікстуру, і властивість для грошової логіки на fast-check. Скрізь — не лише код, а й чому саме так.
Кейс 1. Поле «Ім'я» з лімітом 50: від технік до таблиці значень
Форма реєстрації, поле «Ім'я»: обов'язкове, від 1 до 50 символів. Завдання — не «накидати значень», а вивести кожне з техніки:
| Значення | Звідки взялося | Що ловить |
|---|---|---|
"" | невалідний клас «порожнє» | валідацію обов'язковості |
" " (три пробіли) | окремий невалідний клас | поле, що проходить перевірку обов'язковості й ламає пошук/листи |
" Іван " | неявне припущення про trim | різний trim при записі й при порівнянні (реєстрація обрізає, логін — ні) |
"І" | межа: мінімум | off-by-one на нижній межі |
| рядок з 50 символів | межа: рівно максимум | off-by-one на верхній межі |
| рядок з 51 символа | межа: максимум+1 | що відмова взагалі є |
| рядок з 10 000 символів | клас «валідації немає» | німе обрізання в VARCHAR, розʼїхану верстку |
О'Браєн | Unicode: апостроф | наївне екранування |
Zoë | Unicode: діакритика | нормалізацію NFC/NFD при порівнянні |
😀😀😀 | Unicode: UTF-16 | підрахунок довжини (кожен емодзі — 2 одиниці) |
<script> | невалідний клас: спецсимволи | вивід без санітизації |
Одинадцять значень — і жодного «про всяк випадок»: приберіть будь-який рядок, і конкретна перевірка зникне. А от додати qwerty поруч із О'Браєн — нуль нового покриття: обидва — представники валідного класу.
У Playwright така таблиця природно лягає в параметризацію:
import { test, expect } from '@playwright/test';
const longName = 'а'.repeat(51);
const invalidNames = [
{ value: '', reason: 'порожнє' },
{ value: ' ', reason: 'лише пробіли' },
{ value: longName, reason: 'максимум+1' },
];
for (const { value, reason } of invalidNames) {
test(`ім'я відхиляється: ${reason}`, async ({ page }) => {
await page.goto('/signup');
await page.getByLabel("Ім'я").fill(value);
await page.getByRole('button', { name: 'Зареєструватися' }).click();
await expect(page.getByRole('alert')).toBeVisible();
});
}
Що тут важливо:
- Кожен рядок таблиці — представник свого класу, тому падіння одного тесту одразу називає зламаний клас, а не «щось із валідацією».
' 'і''— окремі рядки свідомо. Якщо об'єднати їх в один кейс, баг «пробіли проходять обов'язковість» сховається за зеленим тестом на порожнє.- Позитивні Unicode-значення (
О'Браєн,Zoë,😀) перевіряються дзеркальним циклом з очікуванням успіху — і саме вони найчастіше червоніють на цілком «готовій» формі.
Кейс 2. «User already exists»: другий прогін червоний
Симптом з життя: сюїт зелений з першого запуску, при повторному — тест реєстрації падає з 409 і «user already exists». Локально після ручного чищення бази знову зелений. Класика: тест створює користувача з фіксованим email qa@example.com, а прибирання стоїть останнім кроком самого тесту — до якого впалий тест не доходить.
Спокуса полікувати «дешево» — замінити фіксований email на qa+${Date.now()}@example.com. Стає краще, але флак повертається у CI з паралельними воркерами: два воркери отримують той самий таймстемп у ту саму мілісекунду — і знову колізія. Правильне лікування — UUID плюс перенесення підготовки і прибирання у фікстуру:
import { test as base } from '@playwright/test';
export const test = base.extend({
freshUser: async ({ request }, use) => {
// унікальність: жодних конфліктів з попереднім прогоном чи сусіднім воркером
const email = `qa+${crypto.randomUUID()}@example.com`;
const res = await request.post('/api/users', { data: { email } });
const { id } = await res.json();
await use({ id, email });
// teardown виконається навіть якщо тест упав
await request.delete(`/api/users/${id}`);
},
});
Що тут важливо:
- Унікальність — гарантія, teardown — гігієна. Навіть якщо
deleteне спрацює (мережа моргнула, API впав), наступний прогін не постраждає: його email інший. Прибирання лише стримує ріст сміття. - Стан створюється через API, не через UI. Реєстрація кліками — це секунди і зайві точки відмови в кожному тесті, якому просто потрібен «якийсь користувач».
example.com— не плейсхолдер з лінощів: RFC 2606 резервує цей домен для тестів, тож лист на таку адресу гарантовано не поїде живій людині — на відміну від «вигаданого»qa@gmail.com, який цілком може належати комусь.
Кейс 3. Властивість для грошей: fast-check замість десятка прикладів
Функції formatAmount (копійки → рядок «1 234,56 ₴») і parseAmount (рядок → копійки). Параметризований тест перевірив би десяток пар «вхід — вихід». Property-based формулює інваріант, який має триматися на всьому діапазоні:
import fc from 'fast-check';
import { formatAmount, parseAmount } from './money';
// Інваріант: парсинг відформатованої суми повертає вихідне число
fc.assert(
fc.property(fc.integer({ min: 0, max: 1_000_000_00 }), (kopiykas) => {
return parseAmount(formatAmount(kopiykas)) === kopiykas;
}),
);
Що тут важливо:
- Суми — цілі копійки, не float. Діапазон задано в
integerсвідомо: двійкове представлення дробів (IEEE 754) робить0.1 + 0.2рівним0.30000000000000004, тому грошова логіка на float — баг за визначенням, ще до всяких тестів. - Shrinking робить половину роботи дебагу. Якщо
parseAmountгубить розряд на сумах від ста тисяч гривень, fast-check не покаже «упало на 73 829 102» — він зведе контрприклад до мінімального100_000_00(рівно сто тисяч), і межа видна одразу. - Кандидати на властивості — чиста логіка: парсери, форматери, розрахунки. Тягти property-based на сценарії з базою і мережею не варто — там кожен прогін залежить від стану, і сотні випадкових входів перетворюються на сотні джерел флаку.
- Той самий принцип сіда діє і тут: fast-check при падінні друкує seed і path — їх треба зберегти в баг-репорті, інакше контрприклад не відтворити.
Дані як продовження технік
- Можу пояснити ланцюжок: техніка виділяє умову → умова стає конкретним значенням → значення лягає в кейс; «my name», «test test», «123» — один клас, покриття як від одного кейсу.
- Знаю різницю вхідних даних (те, що тест вводить) і даних стану (те, що має існувати до кроку) — і що проблеми повторюваності майже завжди про другі.
Небезпечна класика для текстових полів
- Назву чотири категорії напам'ять: порожнє і пробіли, довжина, Unicode, спецсимволи — і для кожної що зазвичай ламається.
- Розумію, чому порожнє поле і поле з пробілів — різні класи, а
" Іван "перевіряє консистентність trim при записі й порівнянні. - Знаю три точки довжини: рівно максимум, максимум+1 і «абсурдно довге» — останнє ловить німе обрізання у
VARCHAR(255)без валідації. - Можу пояснити Unicode-пастки:
😀у JavaScript маєlength === 2(UTF-16), а «é» існує у двох формах — без нормалізації (NFC/NFD) візуально ідентичні імена не збігаються. - Пам'ятаю про
%і_як маски SQL LIKE: пошук100%без екранування знайде і «1000», і «100500».
Дати, таймзони, float і локалі
- Назву крайові дати: 29 лютого, 31 число, 31 грудня → 1 січня, дата народження в майбутньому, до 1970 року, перехід на літній/зимовий час — і моменти близько півночі, бо «сьогодні» клієнта і сервера в UTC — різні дати.
- Знаю, чому
0.1 + 0.2не дорівнює0.3(IEEE 754), чому гроші — у цілих копійках, а в асертах —toBeCloseTo, неtoBe. - Розумію, що
1,5проти1.5і03/07/2026— окремі класи еквівалентності на кожну підтримувану локаль.
Фіксовані набори, генерація, property-based
- Можу обґрунтувати вибір: межі — фіксовано (плата — парадокс пестицидів: ті самі значення з часом перестають знаходити нові баги), масовка — генерацією (faker), інваріанти чистої логіки — property-based (fast-check).
- Правило без винятків: випадковість без зафіксованого чи залогованого сіда — вбудований флак; падіння без сіда не відтворити.
- Можу пояснити property-based за 30 секунд: властивість-інваріант на всьому просторі входів плюс звуження (shrinking) контрприкладу до мінімального.
Ізоляція даних стану
- Впізнаю симптоми поганої ізоляції: зелений локально / червоний у CI, «duplicate entry» на другому прогоні, тест падає лише поза сюїтом.
- Знаю принцип «тест сам створює те, що йому потрібно» і чому UUID в email надійніший і за саме лише прибирання, і за
Date.now(), де паралельні воркери ловлять колізію в ту саму мілісекунду. - Розумію, чому стан готують через API у фікстурі: швидше за UI, а teardown виконається навіть після падіння тесту.
- Знаю, навіщо
example.comі домени.test/.invalid(RFC 2606): лист гарантовано не поїде живій людині.
Анонімізація чутливих даних
- Можу пояснити, чому копія проду на стенді — інцидент безпеки: ті самі персональні дані зі слабшим захистом плюс ризик розсилки справжнім людям.
- Розрізняю маскування, псевдонімізацію і синтетичні дані — і знаю, що за GDPR псевдонімізовані дані лишаються персональними, бо заміна оборотна.
- Пам'ятаю ключову вимогу: заміна має зберігати формат і зв'язки (валідний телефон, один користувач → один псевдонім скрізь), інакше валідація тихо відсікає записи і тести не перевіряють нічого.
У полі імені вже є кейс зі значенням «Іван». Що додає кейс зі значенням «qwerty»?
Питання
Що таке тестові дані з погляду технік тест-дизайну?