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

    02 · Тест-дизайн

    Тестові дані: крайові значення, генерація, ізоляція

    Зміст

    Техніка тест-дизайну відповідає на питання «що перевірити», але кейс не запуститься, поки в ньому не зʼявиться конкретне значення: не «невалідний 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-рівня достатньо розуміти ідею і вміти назвати відмінність: приклад перевіряє точку, властивість — увесь простір входів.

    Влучити в конкретну межу або клас

    Обсяг реалістичних записів

    Інваріант на всьому діапазоні входів

    Що потрібно від даних?

    Фіксовані значення в кейсі

    Генерація: faker

    Property-based: fast-check

    Сід зафіксовано або залоговано

    Влучити в конкретну межу або клас

    Обсяг реалістичних записів

    Інваріант на всьому діапазоні входів

    Що потрібно від даних?

    Фіксовані значення в кейсі

    Генерація: faker

    Property-based: fast-check

    Сід зафіксовано або залоговано

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

    Ізоляція: щоб прогін можна було повторити

    Тепер про дані стану. Симптоми поганої ізоляції впізнаються миттєво: тест зелений локально і червоний у CI; сюїт проходить цілком, але окремий тест сам по собі падає; другий прогін поспіль валиться на «duplicate entry». Причина одна — тести ділять дані: з попереднім прогоном, із сусіднім тестом, із живими користувачами staging-середовища.

    Базовий принцип: тест сам створює те, що йому потрібно, і не покладається на чуже. Два інструменти, які це забезпечують:

    Унікальність замість прибирання. Найдешевша ізоляція — щоразу нові дані: email із UUID не конфліктне ні з попереднім прогоном, ні з паралельним воркером. Прибирання (cleanup) корисне, але покладатися лише на нього не можна: тест, що впав посередині, до прибирання не дійде — і без унікальності наступний прогін успадкує сміття.

    тест упав — teardown є, але
    сміття в базі можливе

    Прогін N

    Тест створює власні дані:
    qa+7f3a…@example.com

    Дія і перевірка

    Прибирання в teardown

    Залишок у базі

    Прогін N+1 зелений

    Унікальність гарантує:
    конфлікту зі сміттям немає

    тест упав — teardown є, але
    сміття в базі можливе

    Прогін N

    Тест створює власні дані:
    qa+7f3a…@example.com

    Дія і перевірка

    Прибирання в teardown

    Залишок у базі

    Прогін N+1 зелений

    Унікальність гарантує:
    конфлікту зі сміттям немає

    Створення через 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 для безпечних тестових адрес.