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

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

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

    Зміст

    Тест робить POST /orders, отримує 202 Accepted, одразу перевіряє 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 таких вольностей не дає: за MDN ресурс (або його опис і посилання на нього) створюється до того, як повернуто відповідь, і новостворене має бути досяжним за URL самого запиту або за URL із заголовка Location. Тому 201, після якого GET за тим самим URL віддає 404, — це порушення контракту : дефект постачальника або неправильно обраний код. Перше, що зʼясовує тестувальник у такій ситуації, — який саме код віддає .

    Сховище читанняЧерга/реплікаAPIТестСховище читанняЧерга/реплікаAPIТестPOST /orders202 (id=42)подія «створено»GET /orders/42 (одразу)читанняще нема404 Not Foundзастосовано (мс–секунди)GET /orders/42 (полінг)читаннязнайдено200 OKСховище читанняЧерга/реплікаAPIТестСховище читанняЧерга/реплікаAPIТестPOST /orders202 (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: брак ізоляції, асинхронність, віддалені сервіси, час і витоки ресурсів. У цій главі нас цікавить зріз недетермінованих зовнішніх залежностей — усього, що застосунок бере зі світу поза власною базою:

    • Сторонні 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 — прямий сигнал «результат ще не готовий», а 201 навпаки обіцяє, що ресурс уже досяжний, тож 201 із наступним 404 — дефект.
    • Фіксований sleep програє завжди: або флак, або втрачений час. Заміна — полінг на конкретну умову з верхнім таймаутом та ідемпотентною перевіркою.
    • Сідь стан через публічний API за замовчуванням; пряма БД — лише для дорогого чи недосяжного стану, і з порушити бізнес-інваріанти.
    • Ізоляцію дає власність над даними: унікальні ідентифікатори, окремий акаунт або tenant. Це зазвичай надійніше за прибирання після тесту — головно тому, що з прибиранням баг і падіння розʼїжджаються по різних тестах.
    • Тест відповідає лише за створене ним і не спирається на чужі дані спільного стенду; недетерміновані залежності (час, сторонні API, рандом) — мокати або фіксувати даними.

    Можливі питання

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

    Джерела

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

    • MDN — 202 Accepted202 означає, що запит прийнято, а обробка не завершена або навіть не почалася; відповідь non-committal, і каналу для пізнішого результату протокол не дає.
    • MDN — 201 Created — для контрасту: при 201 ресурс створено до відповіді й він зобовʼязаний бути досяжним за URL запиту або Location.
    • Apache Kafka — Introduction (документація 4.0) — джерело лагу на рівні інфраструктури: реплікація виконується на рівні топіка, тож читання може обслуговувати не той брокер, що приймав запис.
    • AWS — DynamoDB read consistency — «eventually consistent» — офіційна назва режиму читання за замовчуванням: відповідь може не відображати щойно завершений запис.

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

    • Martin Fowler — Eradicating Non-Determinism in Tests — заборона сформульована прямо: «Never use bare sleeps to wait for asynchonous responses: use a callback or polling», і стеля очікування має бути навмисно високою.
    • Playwright — page.waitForTimeout (Discouraged) — той самий висновок канонічно: метод паузи позначено як Discouraged, а тести, що чекають на час, названо «inherently flaky».
    • Playwright — Test assertions (expect.poll, toPass)expect.poll перетворює синхронний асерт на полінг: дефолтний таймаут — 5 секунд, інтервали [100, 250, 500, 1000] мс, timeout: 0 вимикає стелю.

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

    • ISTQB CTFL Syllabus v4.0.1 — підготовка даних — планована активність процесу: вимоги до визначає тест-дизайн, а створює їх test implementation.
    • Playwright — API testing — підготовка серверного стану перед відкриттям застосунку — офіційно назване застосування контексту запитів, а не обхідний шлях.

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

    • xUnit Test Patterns — Erratic Test — взаємодія тестів зазвичай спричинена спільною фікстурою, а паралельний прогін над спільним станом (Test Run War) можливий лише за її наявності.
    • Martin Fowler — Eradicating Non-Determinism in Tests — дві школи: ізоляція проти явно оголошених залежностей із заданим порядком; джерело віддає перевагу ізоляції за гнучкість.

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

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

    • Testcontainers — Getting started — спільні для кількох користувачів чи ресурси роблять результати недетермінованими через псування даних і дрейф конфігурації.
    • Testcontainers (титульна сторінка) — ефемерна альтернатива: одноразові інстанси залежностей, які створюються й видаляються разом із прогоном, дають «known state» на старті.
    • xUnit Test Patterns — Erratic Test — маркер чужого впливу: тест сам по собі зелений, а в сюїті падає від додавання чи вилучення іншого.

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

    • MDN — 201 Created201 обіцяє досяжність новоствореного ресурсу, тож 201 із наступним 404 — дефект контракту, а не привід для полінгу.
    • Playwright — page.waitForTimeout (Discouraged) — пауза дозволена лише для дебагу; заміна — не довша пауза, а сигнал або умова.
    • xUnit Test Patterns — Erratic Test — «перший запуск інакший» — окремий патерн Unrepeatable Test із тією самою причиною: спільна фікстура.
    • Martin Fowler — Eradicating Non-Determinism in Tests — час і віддалені сервіси стоять у переліку джерел недетермінізму поряд із браком ізоляції.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.