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

    04 · API-тестування

    Тестові дані та стан в API-тестах

    Зміст

    Тест робить POST /orders, отримує чистий 201 Created, одразу перевіряє GET /orders/42 — і бачить 404. Замовлення «зникло». Ти йдеш дивитися логи бекенду, розробник знизує плечима: у нього все створюється. За півгодини з'ясовується, що баг не в застосунку і не в тесті як такому — а в тому, що тест поставив запитання на мілісекунду раніше, ніж система встигла на нього відповісти. Це не виняток, а типова причина «плаваючих» падінь на рівні API.

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

    Eventual consistency: чому створене «ще не існує»

    У маленькому монолітному застосунку POST записує рядок у базу, і той самий рядок одразу видно на GET. Але щойно система масштабується, між записом і читанням з'являється затримка. Це і є кінцева узгодженість (eventual consistency): система гарантує, що дані стануть узгодженими колись потім, а не в ту саму мить.

    Звідки береться лаг:

    • Черги повідомлень. POST кладе подію в чергу (Kafka, RabbitMQ) і повертає 202 Accepted — «прийняв, обробляю». Фактичний запис станеться асинхронно, за десятки-сотні мілісекунд.
    • Репліки на читання. Запис іде в майстер-базу, а GET обслуговує репліка. Реплікація не миттєва — це replication lag.
    • Пошукові індекси. Створений об'єкт лягає в основну базу одразу, але в пошуковий індекс (Elasticsearch тощо) потрапляє з окремим циклом оновлення, часто близько секунди.
    • Кеші. Список уже закешовано, і новий елемент з'явиться в ньому лише після інвалідації (механіку кешування розбирає глава Кешування).

    Ключова ознака: код відповіді 202 Accepted замість 201 Created — це відкритий сигнал сервера «результат ще не готовий, не читай наосліп». Але лаг буває й за 201, якщо читання йде іншим шляхом, ніж запис.

    Сховище читанняЧерга/реплікаAPIТестСховище читанняЧерга/реплікаAPIТестPOST /orders201 (id=42)подія «створено»GET /orders/42 (одразу)читанняще нема404 Not Foundзастосовано (мс–секунди)GET /orders/42 (полінг)читаннязнайдено200 OKСховище читанняЧерга/реплікаAPIТестСховище читанняЧерга/реплікаAPIТестPOST /orders201 (id=42)подія «створено»GET /orders/42 (одразу)читанняще нема404 Not Foundзастосовано (мс–секунди)GET /orders/42 (полінг)читаннязнайдено200 OK

    Мораль: між «створив» і «побачив» у розподіленій системі є вікно неузгодженості. Тест мусить його пережити, а не вдавати, що його немає.

    Полінг із таймаутом замість sleep

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

    • Пауза занадто коротка — на завантаженому CI-агенті лаг цього разу склав 2.5 секунди, і тест падає. Плаваючий флак.
    • Пауза занадто довга — насправді дані готові за 200 мс, але кожен тест дарма стоїть 2 секунди. Сюїта з 300 тестів втрачає десять хвилин на порожньому місці.
    // Погано: фіксована пауза — або флак, або марна трата часу
    await new Promise((r) => setTimeout(r, 2000));
    const res = await request.get(`/api/orders/${id}`);
    expect(res.status()).toBe(200);

    Правильний підхід — полінг (polling): періодично перепитувати систему, доки умова не справдиться, з жорстким верхнім таймаутом. Тест іде далі рівно тоді, коли дані реально з'явилися, і падає, лише якщо їх немає дуже довго.

    // Добре: чекаємо саме на умову, з таймаутом
    await expect.poll(async () => {
      const res = await request.get(`/api/orders/${id}`);
      return res.status();
    }, { timeout: 10_000, intervals: [250, 500, 1000] }).toBe(200);

    Три речі роблять полінг надійним:

    1. Умова, а не час. Чекаємо на конкретний спостережуваний факт (статус 200, поле status: "processed", наявність елемента у списку), а не на абстрактні «дві секунди».
    2. Верхній таймаут. Полінг без стелі — це вічне зависання, гірше за падіння. Таймаут перетворює «система тупить нескінченно» на чесний фейл.
    3. Ідемпотентна перевірка. Функція всередині полінгу має бути безпечною для багаторазового виклику — тільки читання, без побічних ефектів. Полити POST не можна: створиш десять замовлень.

    Полінг — це API-варіант «розумного очікування», яке в браузері дає авточекання Playwright. Ширший розбір нестабільності саме API-тестів — у главі Стабільність API-тестів.

    Сідінг даних: через публічний API чи напряму в БД

    Майже кожен тест потребує передумови: «існує користувач з роллю admin», «є замовлення в статусі paid». Створювати цей стан руками щоразу — дорого, тож його сідять (seed) програмно. Два принципові шляхи, і вибір між ними — постійне джерело суперечок.

    Сідінг через публічний API — тест викликає ті самі ендпоінти, що й реальний клієнт (POST /users, POST /orders). Сідінг напряму в БД — тест пише INSERT у таблиці в обхід застосунку (механіку SQL і підключення до бази розбирає розділ про бази даних).

    КритерійЧерез APIНапряму в БД
    Дані валідні за бізнес-правиламиТак, їх ставить сам застосунокЛегко порушити інваріанти
    ШвидкістьПовільніше (мережа, логіка)Швидко, один запит
    Крихкість до змін схемиСтійкіше (контракт API)Ламається на кожній міграції
    Доступність із CIПотрібен лише HTTPПотрібен доступ до БД
    Складний/недосяжний станНе завжди можливо через APIМожна поставити будь-що

    Правило за замовчуванням: сідь через API, поки можеш. Дані, створені застосунком, гарантовано узгоджені — правильні зовнішні ключі, обчислені поля, події в черзі. Прямий INSERT цього не робить: ти можеш вставити замовлення, під яке застосунок ніколи не згенерував би рахунок, і потім годину дивуватися «неможливому» стану.

    Пряма БД виправдана, коли стан дорого або неможливо відтворити через API: історичні дані «за минулий рік», стан з іншого мікросервісу, спеціально «зіпсований» запис для негативного сценарію. І навіть тоді краще шукати проміжний варіант — тестовий бекдор-ендпоінт (наприклад, POST /test/seed), який доступний лише на не-продакшн-середовищах і ставить стан валідно, але швидко.

    Так

    Ні

    Так

    Ні

    Потрібен стан для тесту

    Є публічний API
    для створення?

    Сідити через API

    Стан дорогий
    чи недосяжний через API?

    Пряма БД або тест-бекдор
    обережно

    Так

    Ні

    Так

    Ні

    Потрібен стан для тесту

    Є публічний API
    для створення?

    Сідити через API

    Стан дорогий
    чи недосяжний через API?

    Пряма БД або тест-бекдор
    обережно

    Сідінг через API природно поєднується з патерном API-клієнта й ланцюжками запитів — це тема глави API-автотести в коді.

    Ізоляція: свій акаунт, свій tenant

    Другий великий клас проблем — не час, а зіткнення даних. Два тести (або два паралельних воркери, або ти й колега на тому самому стенді) чіпають той самий об'єкт, і один затирає стан іншого. Тест «іноді» бачить не те, що сам створив.

    Базовий принцип ізоляції: кожен тест володіє своїми даними й не спирається на чужі. Практичні рівні ізоляції в API-тестах:

    • Унікальні ідентифікатори. Ніколи не хардкодь email: "test@test.com" — на другому паралельному запуску буде конфлікт унікальності. Генеруй унікальне значення на кожен прогін.

      import { faker } from '@faker-js/faker';
      
      const email = `qa+${Date.now()}-${faker.string.alphanumeric(6)}@example.com`;
    • Окремий акаунт на тест або на воркер. Якщо тести змінюють профіль, налаштування, баланс — дай кожному свого користувача. Тоді паралельні тести фізично не бачать даних одне одного. Керування токенами таких акаунтів — у главі Авторизація в API.

    • Окремий tenant. У багатоорендних (multi-tenant) SaaS-системах дані розділені за орендарем (організацією, робочим простором). Виділити тесту власний tenant — найсильніша ізоляція: не перетинаються не лише записи, а й довідники, ліміти, налаштування. Один орендар не бачить даних іншого за побудовою системи.

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

    Детермінованість зовнішніх залежностей

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

    • Сторонні API. Платіжний шлюз, курс валют, геолокація, погода. Реальний виклик у тесті означає, що твій зелений/червоний залежить від чужого аптайму й чужих даних.
    • Час. Логіка «підписка активна ще 3 дні», «купон діє до кінця місяця» дає різну відповідь сьогодні й завтра. Тест, зелений у липні, червоніє 1 числа наступного місяця.
    • Випадковість. Промокоди, A/B-когорти, рандомні ідентифікатори роблять відповідь непередбачуваною.

    Стратегії приборкання:

    • Мокати залежність. Замінити реальний сторонній сервіс контрольованим дублером (стаб, mock-сервер), який завжди повертає відоме. Це основний інструмент детермінізму на рівні сервісів — механіка в главі Мокання залежностей.
    • Використати офіційний sandbox. Багато платіжних і поштових провайдерів дають тестове середовище з передбачуваними «магічними» значеннями (картка, що завжди відхиляється; сума, що завжди проходить).
    • Керувати часом через дані, а не через тест. З API-тесту ти зазвичай не можеш перевести годинник сервера. Тому не «підлаштовуйся під сьогодні», а став дату явно: створюй підписку з expiresAt за рік уперед, купон — з датою в майбутньому. Тоді результат не залежить від дня прогону.

    Окремий бік детермінізму — ідемпотентність повторних запитів (повторний PUT/DELETE, Idempotency-Key на POST): без неї ретрай тесту чи мережевий збій створює дублі. Це тема глави Тест-дизайн для API.

    Спільне оточення і чужі дані

    Ідеал — свіжа ізольована база на кожен прогін. Реальність — один staging-стенд, на якому одночасно працюють автотести, мануальники, розробники й демо для замовника. Це спільне оточення (shared environment) з усіма його «сусідами по кімнаті».

    Головні пастки спільного стенду:

    • Залежність від передіснуючих даних. Тест припускає, що «користувач з id=1 — це адмін Іван». Хтось перейменував Івана або видалив запис — тест падає, хоча застосунок здоровий. Не спирайся на дані, яких сам не створював.
    • Асерти на глобальні лічильники. Перевірка «GET /users повертає рівно 5 записів» приречена: на спільному стенді користувачів то 5, то 47. Перевіряй наявність свого створеного запису у відповіді, а не точну кількість усіх.
    • Гонки між воркерами. Два паралельних тести беруть «перший вільний промокод» — і хапають один і той самий. Вирішується виділенням даних на воркер (див. ізоляцію вище).
    • Ефемерні середовища. Радикальний вихід — не ділити стенд узагалі, а піднімати чисте оточення на кожен прогін (докеризована база, ефемерний namespace). Дорожче в інфраструктурі, але прибирає весь клас «чужих даних».

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

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

    • Виглядає як баг створення (POST нібито не записує), а насправді читання йде з репліки чи індексу з лагом — потрібен полінг, а не баг-репорт.
    • Виглядає як надійна пауза sleep(2000), а насправді на завантаженому CI двох секунд бракує — плаваючий флак замість детермінованого очікування.
    • Виглядає як незалежний тест, а насправді він спирається на юзера id=1, якого хтось перейменував на спільному стенді — чужі дані.
    • Виглядає як детермінований сценарій, а насправді відповідь залежить від реального курсу валют або поточної дати — незамокана зовнішня залежність.
    • Виглядає як ізоляція «бо є afterEach з прибиранням», а насправді тест упав до клінапу й лишив сміття, а наступний прогін наткнувся на дубль — унікальні дані надійніші за прибирання.
    • Виглядає як швидкий сід через прямий INSERT, а насправді він обійшов інваріанти застосунку й створив стан, який продукт ніколи не згенерував би — «неможливий» баг у тесті.

    Підсумок

    • У розподіленій системі між записом і читанням є вікно неузгодженості (eventual consistency); 202 Accepted — прямий сигнал «результат ще не готовий».
    • Фіксований sleep програє завжди: або флак, або втрачений час. Заміна — полінг на конкретну умову з верхнім таймаутом та ідемпотентною перевіркою.
    • Сідь стан через публічний API за замовчуванням; пряма БД — лише для дорогого чи недосяжного стану, і з ризиком порушити бізнес-інваріанти.
    • Ізоляцію дає власність над даними: унікальні ідентифікатори, окремий акаунт або tenant. Це надійніше за прибирання після тесту.
    • Тест відповідає лише за створене ним і не спирається на чужі дані спільного стенду; недетерміновані залежності (час, сторонні API, рандом) — мокати або фіксувати даними.

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

    • «Тест створює запис і одразу його не бачить. Що не так?» Інтерв'юер перевіряє, чи згадаєш eventual consistency (репліки, черги, індекси, 202), а не кинешся правити тест хардкодом. Сильна відповідь: спершу з'ясувати, синхронна операція чи асинхронна, і замінити гонку полінгом на умову.
    • «Чому sleep — погано і чим його замінити?» Класика на розуміння флаку. Треба показати, що фіксована пауза погана в обидва боки, і описати полінг: умова замість часу, таймаут, ідемпотентність перевірки.
    • «Як готуєш тестові дані: через API чи прямо в базу?» Дивляться на зрілість. Очікують не догму, а компроміс: за замовчуванням API (валідність, стійкість до міграцій), пряма БД — виняток для дорогого стану, з усвідомленням ризику обійти інваріанти.
    • «Як ізолюєш тести на спільному стенді?» Перевіряють, чи мислиш власністю над даними: унікальні значення, окремий акаунт/tenant, а не «почистимо базу перед прогоном». Плюс — згадка гонок між паралельними воркерами.
    • «Тест залежить від сьогоднішньої дати / зовнішнього сервісу. Що зробиш?» Хочуть почути про детермінізм: мок або sandbox для сторонніх залежностей, а час контролювати через явні дати в сіді, а не через реальний годинник.

    Джерела

    • MDN — 202 Accepted — семантика асинхронної відповіді «прийнято, обробляю».
    • MDN — 201 Created — синхронне створення ресурсу для контрасту з 202.
    • Playwright — Test assertions (expect.poll, toPass) — офіційна механіка полінгу на умову з таймаутом.
    • Playwright — API testingAPIRequestContext для сідінгу стану й перевірок через API.
    • Силабус ISTQB CTFL 4.0 (istqb.org) — підготовка тестових даних і тестового середовища як частина тестового процесу (test implementation).