API-автотести в коді: клієнти, структура, ланцюжки
Зміст
Postman і Newman добре тримають першу сотню запитів. Але коли перевірок стають тисячі, коли їх треба ганяти в CI на кожен PR, перевикористовувати авторизацію, ділити між прогонами й читати падіння в — колекція перетворюється на клубок скриптів, який ніхто не наважується чіпати. Тут API-тести переїжджають у код: той самий мовний стек, той самий репозиторій і той самий рев'ю-процес, що й у продукту.
Ця глава — про інженерію API-тестів, а не про тест-дизайн (техніки — у главі про тест-дизайн для API) і не про мову як таку (JS/TS — окремий розділ). Вона відповідає на питання, які відрізняють того, хто писав автотести руками, від того, хто копіпастив приклади: «чим ти ходиш в API з коду», «як готуєш стан», «чому fetch не впав на 500», «ретраї — це добре чи погано». (: базовий JS/TS.)
HTTP-клієнти: чим ходити в API
«HTTP-клієнт» — це бібліотека, що вміє відправити запит і повернути відповідь як об'єкт, з яким працює твій код. Вибір клієнта — не питання смаку: він визначає, як ти обробляєш помилки, й серіалізацію. Чотири інструменти, які реально зустрічаються в JS/TS-проєктах:
| Клієнт | Де живе | Кидає на 4xx/5xx | Типовий контекст |
|---|---|---|---|
fetch | Вбудований у браузер і в Node.js (з 18-ї версії) | Ні | Легкі перевірки без залежностей |
axios | Зовнішня бібліотека | Так (за замовчуванням) | Окремі API-сюїти, багато проміжної логіки |
APIRequestContext | Playwright | Ні за замовчуванням (failOnStatusCode вмикає) | API-тести й підготовка стану поруч з e2e |
supertest | Зовнішня бібліотека | Ні (асертиться окремо) | Тестування свого сервера в процесі |
Ключова відмінність: fetch не вважає HTTP-помилку помилкою. Статус 404 чи 500 — це успішно виконаний запит, у якого просто response.ok === false. А от відхиляється fetch на частину інших умов, і перелік у документації відкритий: MDN називає серед них мережевий збій і невідповідну схему URL, а окремо описує відхилення AbortError, коли запит скасували через AbortController. Тому наївний код нижче ловить не те, що здається:
// Пастка: catch НЕ спрацює на 500 — fetch вважає це успіхом
try {
const res = await fetch('https://api.example.com/orders');
const body = await res.json();
} catch (e) {
// сюди зайдемо на мережевому збої, кривій схемі чи скасуванні — але не на 5xx
}
axios поводиться протилежно: на будь-який статус поза діапазоном 2xx він відхиляє проміс (це регулюється опцією validateStatus). Зручно для продакшн-коду, але для тестів радше заважає — ти й так хочеш перевіряти статус явно, а не через try/catch. APIRequestContext ( request у Playwright) поводиться як fetch: помилка HTTP — це нормальна відповідь, яку ти . Це дефолт із перемикачем: опція failOnStatusCode вмикає кидання на коди поза 2xx і 3xx — тобто 301 помилкою не вважається й там. Його бонус — інтеграція з рештою Playwright: власний baseURL, запис запитів у трейс і з браузерним контекстом через page.request/context.request (зручно для «залогінься через API, а перевіряй в UI»); сама фікстура request — ізольований контекст, cookie сторінки вона не бачить.
supertest стоїть окремо: він призначений тестувати HTTP-сервер у тому ж процесі, піднімаючи його на ефемерному порту. Ти передаєш йому свій застосунок (наприклад, Express) — це швидко й без мережі, але працює лише коли сервер під рукою в коді. Для тестування задеплоєного API по URL він не потрібен.
Практичний висновок: для більшості продуктових API-, що ходять по мережі до реального середовища, природний вибір — APIRequestContext (якщо стек уже на Playwright) або axios/fetch у самостійній сюїті. supertest — це про компонентні тести бекенду зсередини.
Анатомія API-тесту: arrange–act–assert
Структура тесту не залежить від того, UI це чи API. Канон — arrange–act–assert (AAA), «підготуй — дій — перевір» (детально — у розділі про архітектуру автотестів). Для API три фази читаються так:
- Arrange — усе, що має існувати до головного запиту: токен авторизації, створений через API ресурс, вихідний стан бази. Це не предмет перевірки, а декорації сцени.
- Act — рівно один запит, поведінку якого перевіряє тест. Якщо в тесті два «головні» запити — це два тести.
- Assert — перевірки відповіді: статус, тіло, заголовки, відповідність схемі (повний чек-лист — у главі про перевірки відповіді).
test('POST /orders створює замовлення й повертає 201 зі схемою Order', async ({ request }) => {
// Arrange
const token = await auth.loginAs('customer');
// Act
const res = await request.post('/orders', {
headers: { Authorization: `Bearer ${token}` },
data: { productId: 42, qty: 2 },
});
// Assert
expect(res.status()).toBe(201);
const body = await res.json();
expect(body).toMatchObject({ productId: 42, qty: 2, status: 'created' });
});
Два інваріанти, які роблять тест придатним до життя. По-перше, назва описує перевірку, а не дію: «повертає 201 зі схемою Order», а не «тест POST orders» — тоді червоний тест у звіті вже сам розповідає, що зламалось. По-друге, тест не залежить від порядку: він сам готує свій стан і не розраховує, що попередній тест щось лишив. Це передумова для паралельного запуску, до якого ми ще дійдемо.
Патерн API-клієнта
Якщо в кожному тесті писати сирий request.post('/orders', { headers: ... }), то заголовок авторизації, базовий шлях і розбір відповіді розповзуться копіпастою по всій сюїті. Змінився формат авторизації — правиш у ста місцях. Розв'язання те саме, що дає для UI: патерн API-клієнта — клас, який ховає за методом з людською назвою.
class OrdersApi {
constructor(private request: APIRequestContext, private token: string) {}
createOrder(data: OrderInput) {
return this.request.post('/orders', {
headers: { Authorization: `Bearer ${this.token}` },
data,
});
}
getOrder(id: string) {
return this.request.get(`/orders/${id}`, {
headers: { Authorization: `Bearer ${this.token}` },
});
}
}
Тепер тест читається як сценарій, а не як HTTP-механіка: await orders.createOrder({ productId: 42, qty: 2 }). Базовий URL, авторизація, серіалізація живуть в одному місці — це єдине джерело істини (принципи дизайну тестового коду — канон розділу про архітектуру). Клієнт віддає «сиру» відповідь, а перевірки лишаються в тесті: змішувати асерти всередину клієнта — спокуса, яка перетворює його на і ховає, що саме перевіряється.
Кожен шар знімає з тесту одну відповідальність: HTTP-клієнт — про транспорт, API-клієнт — про словник ендпоінтів, тест — лише про перевірку. Ту саму ідею потім перевикористовуєш і для підготовки даних в e2e-тестах.
Ланцюжки й підготовка стану через API
Багато перевірок не мають сенсу «на порожньому місці»: щоб перевірити GET /orders/{id}, замовлення спершу треба створити; щоб перевірити скасування — воно має існувати й бути в правильному стані. Так виникають ланцюжки (chaining): відповідь одного запиту дає вхід для наступного. Класика — витягнути id зі створеного ресурсу й підставити його в наступний виклик.
Головне правило: готуй стан найдешевшим надійним способом, а не через UI. Якщо тесту потрібне замовлення, не клацай його майстром у браузері — створи запитом. API- на порядок швидший і стабільніший за клікання, а тому не додає до сценарію, який насправді про інше. (Стратегія тестових даних і стану — окрема глава цього розділу; канон стратегії даних — у розділі про архітектуру.)
Два підводні камені ланцюжків. Перший — (teardown): те, що тест насіяв, він і має прибрати, інакше середовище засмічується й сусідні прогони починають бачити чужі дані. Прибирання вішають на after-хук, щоб воно відпрацювало навіть коли основна перевірка впала, — але саме тут ховається пастка, про яку попереджає Фаулер: виняток усередині прибирання лишає тест зеленим, а ламає ізоляцію для наступних. Тож за станом самого прибирання треба стежити окремо. Другий — eventual consistency: у розподілених системах створений ресурс не завжди видно наступним запитом миттєво (репліка ще не наздогнала). Лікується це не sleep-ом навмання, а із таймаутом — до цього повернемось нижче й детально в главі про стабільність.
Валідація схем: ajv і zod
Перевіряти тіло відповіді пополе (expect(body.id).toBeDefined()) виснажливо й дірчасто: забув поле — пропустив . Надійніше перевірити всю форму відповіді одним асертом проти схеми. Два робочі інструменти в JS/TS:
- ajv — валідатор JSON Schema. Ти описуєш очікувану структуру мовою JSON Schema, ajv компілює її у швидку функцію-валідатор і повертає список розбіжностей. Головна вигода — JSON Schema не залежить від мови, тож ту саму схему можна взяти прямо з OpenAPI-специфікації і фактично перетворити доку на тест (в OpenAPI 3.1 схеми — це повноцінна JSON Schema; у 3.0 — близький діалект, дрібниці на кшталт
nullableдоведеться конвертувати). - zod — TS-first бібліотека: схему описуєш кодом на TypeScript, а
parseперевіряє дані в рантаймі. Бонус — з тієї ж схеми виводиться статичний тип, тож після валідації ти працюєш із типізованим об'єктом без ручнихas.
import { z } from 'zod';
const Order = z.object({
id: z.string().uuid(),
productId: z.number().int(),
qty: z.number().int().positive(),
status: z.enum(['created', 'paid', 'cancelled']),
});
const body = await res.json();
const order = Order.parse(body); // кине, якщо форма не збіглась; далі order типізований
Груба евристика вибору: якщо схеми вже є в OpenAPI й хочеться однієї правди для доки й тестів — ajv; якщо тести пишуться на TS з нуля й потрібні статичні типи — zod. Важлива дисципліна: за замовчуванням забороняй зайві поля — у JSON Schema це additionalProperties: false, у Zod — z.strictObject() (методи .strict() і .passthrough() задепрекейчено на користь функцій верхнього рівня). Без такої заборони інструменти поводяться по-різному: JSON Schema без additionalProperties зайве поле просто пропускає, а Zod за замовчуванням вирізає нерозпізнані ключі з результату розбору; «пропустити наскрізь» — це в Zod окремий, третій режим, z.looseObject(). Схема ловить структурні регресії дешево — розвиває цю ідею далі в окремій главі.
Таймаути й ретраї
Таймаут відповідає на питання «як довго чекати на відповідь, перш ніж вважати запит провальним». Без нього завислий запит підвісить весь прогон, поки в справу не втрутиться глобальний таймаут з незрозумілою помилкою. Тому таймаут ставлять свідомо — і на рівні клієнта (окремий запит), і на рівні тесту. Значення обирають з голови сервісу: занизьке породжує на повільному CI, зависоке ховає деградацію продуктивності.
(retry) — це автоматичний повтор запиту. І це найтонше місце теми, бо ретрай легко перетворюється на килим, під який замітають баги. Розрізняй три різні речі, які часто плутають:
- Мережевий ретрай на ідемпотентний запит. Повторити
GET, що впав через скидання з'єднання, — безпечно й нормально: повтор ідемпотентного запиту лишає стан сервера таким, яким його зробив перший виклик (аGETйого й так не змінює). Повторювати жPOST, який щось створює, наосліп — небезпечно: ризикуєш дублем (тут рятуєIdempotency-Key, якщо API його підтримує). Памʼятай при цьому, що інструмент цього правила не знає: опціяmaxRetriesу Playwright повторює лише помилкуECONNRESET, без огляду на метод і без огляду на HTTP-код відповіді, а за замовчуванням дорівнює0. - Полінг для eventual consistency. Коли даних ще немає, бо система не встигла узгодитись, — це не ретрай запиту, а очікування стану: періодично перепитувати
until-умову з таймаутом. У Playwright для цього єexpect.poll. Це легітимний патерн, а не маскування. - Ретрай усього тесту, що впав. Ось це — червоний . Якщо тест «зеленіє з другого разу», ретрай не полагодив причину, а сховав флак. Ретраї на рівні ранера доречні як тимчасовий буфер і сигнал («цей тест нестабільний»), але не як лікування (політика ретраїв і — канон розділу про архітектуру; API-специфіка — глава про стабільність).
Одним рядком: таймаут і полінг лікують чесну асинхронність; сліпий ретрай мовчки ховає баг.
Паралельний запуск
API-тести швидкі (немає браузера й рендеру), тож їх ганяють десятками одночасно — прогін падає з десятків хвилин до одиниць. Але паралельність безжально карає за одну річ: спільний стан. Два воркери, що правлять один ресурс, дають гонку, і тест «іноді падає» без видимої причини.
Головна передумова безпечної паралельності — ізоляція даних. Кожен тест працює зі своїми даними: унікальний email через генератор, окреме замовлення, а краще — окремий акаунт чи tenant на воркер. Варто знати, що ізоляція — не єдина школа: дехто робить менший наголос на ній і більший на явно визначених залежностях, які змушують тести йти в заданому порядку. Фаулер, на якого спирається ця глава, віддає перевагу саме ізоляції — за гнучкість. Антипатерн — один спільний тестовий юзер на всю сюїту: щойно тести стартують паралельно, вони стають у чергу за його станом і починають перетирати одне одному дані. (Концепція паралелізації й ізоляції — канон розділу про архітектуру; тут — лише API-проєкція.)
Практична дрібниця, що рятує нерви: якщо API віддає глобальний лічильник, послідовні id чи спільний кошик — закладай це в дизайн даних одразу, бо в паралелі такі місця «стріляють» першими.
Типові помилки
- Виглядає як «тест ловить помилку сервера», а насправді
fetchмовчки її проковтнув.fetchіAPIRequestContextне кидають на 4xx/5xx — статус треба перевіряти явним асертом, а не сподіватись наcatch. - Виглядає як стабільна перевірка тіла, а насправді дірка в . Перевірка кількох полів пропускає зникнення чи зміну типу інших. Асерть всю форму проти схеми — і забороняй зайві поля.
- Виглядає як «баг у продукті — ресурс не з'явився», а насправді eventual consistency. Наступний запит прийшов раніше, ніж репліка узгодилась. Лікується полінгом із таймаутом, а не
sleep(3000). - Виглядає як стабілізація завдяки ретраям, а насправді замаскований флак. Тест, що зеленіє з другої спроби, досі зламаний — ти лише перестав це бачити у звіті.
- Виглядає як незалежні тести, а насправді прихована залежність через дані. Один спільний акаунт чи послідовність тестів «А створює — Б читає» валиться, щойно вмикається паралель.
- Виглядає як зручний API-клієнт, а насправді god object. Клієнт із асертами всередині ховає, що саме перевіряється, і плутає транспорт з перевіркою. Перевірки — у тесті.
Підсумок
- HTTP-клієнти діляться за поведінкою на помилку:
fetchіAPIRequestContextне кидають на 4xx/5xx (у Playwright це дефолт із перемикачемfailOnStatusCode, межа якого — поза 2xx і 3xx),axiosкидає за замовчуванням;supertest— про тестування свого сервера в процесі. - Структура API-тесту — arrange–act–assert: один головний запит на тест, стан готується заздалегідь, назва описує перевірку.
- Патерн API-клієнта ховає ендпоінти, авторизацію й базовий URL за методами; перевірки лишаються в тесті, не в клієнті.
- Стан готуй через API, не через UI; ланцюжки прибирай за собою в after-хуці; eventual consistency лікуй полінгом, а не паузами.
- Валідація проти схеми (ajv або zod) ловить структурні регресії одним асертом; забороняй зайві поля.
- Паралельність тримається насамперед на ізоляції даних (конкурентна школа — явні залежності й заданий порядок); сліпий ретрай усього тесту маскує флак, а не лікує його.
Можливі питання
- «Чим ти ходиш в API з коду і чому саме цим?» — інтерв'юер перевіряє, чи є реальний досвід за назвами бібліотек. Сильна відповідь називає 2–3 клієнти й пояснює компроміс, а не хвалить один.
- «Чому
fetchне впав на статусі 500?» — класична пастка на розуміння, що HTTP-помилка ≠ помилка виконання запиту. Хто плутає, той писав тільки . - «Як готуєш тестові дані для API-тесту?» — дивляться, чи згадаєш API-сідінг замість UI, ізоляцію й прибирання. Відповідь «створюю руками в базі» насторожує.
- «Ретраї в тестах — це добре чи погано?» — перевірка зрілості. Очікують розрізнення: мережевий ретрай ідемпотентного запиту й полінг стану — ок; ретрай усього тесту, що падає, — маскування флаку.
- «Що потрібно, щоб запускати API-тести паралельно?» — ключове слово «ізоляція даних»; згадка про спільний акаунт як антипатерн — сильний сигнал, а згадка про альтернативу з явними залежностями й порядком — ще сильніший.
- «Як перевіряєш тіло відповіді?» — очікують згадку про валідацію схеми (JSON Schema/ajv або zod) і заборону зайвих полів, а не лише
expectпо кількох полях.
Джерела
HTTP-клієнти: чим ходити в API
- MDN — Using the Fetch API —
fetchне відхиляється на HTTP-помилку — статус треба перевіряти самому; відхилення описано відкритим переліком, аResponse.okістинний у діапазоні 200. - Playwright — class APIRequestContext — «не кидає на
4xx/5xx» — дефолт із перемикачемfailOnStatusCode, межа якого — поза2xxі3xx; кукі спільні з браузерним контекстом черезpage.request. - Playwright — API testing — фікстура
requestуспадковуєbaseURL,extraHTTPHeadersі з конфігу, а під капотом викликаєapiRequest.newContext().
Анатомія API-тесту: arrange–act–assert
- xUnit Test Patterns — Four-Phase Test — чотири послідовні фази — setup, exercise, verification, teardown — щоб читач одразу бачив, яку саме поведінку перевіряє тест.
- Martin Fowler — GivenWhenThen — Given/When/Then — переформулювання тих самих фаз, а назва Arrange-Act-Assert належить Біллу Вейку.
- Martin Fowler — PageObject — той самий принцип, що й для інтерфейсу: обгортка ховає механіку доступу за методами предметної мови.
- Playwright — API testing — спільні налаштування клієнта (базовий URL, заголовки) живуть у конфігурації контексту, а не в кожному тесті.
Ланцюжки й підготовка стану через API
- Playwright — API testing — підготовка серверного стану до сценарію й перевірка серверних пост-умов — офіційно названі застосування контексту запитів.
- xUnit Test Patterns — Four-Phase Test — прибирання має відпрацювати навіть тоді, коли асерт кинув виняток; водночас воно не має затуляти логіку тесту.
- MDN — 202 Accepted — асинхронний бік:
202означає, що запит прийнято до обробки, але вона ще не завершена — звідси й вікно неузгодженості.
- Ajv — Why use Ajv (офіційна документація) — декларативна схема замість коду перевірок: Ajv компілює JSON Schema у швидку функцію, яка в TypeScript працює як type guard.
- Zod — Introduction (офіційна документація) — TypeScript-first: результат
parseодразу перевірений і типобезпечний, а схему можна конвертувати назад у JSON Schema. - Zod — API (Objects) — режими зайвих ключів: дефолт вирізає нерозпізнані ключі,
z.strictObject()кидає,z.looseObject()пропускає наскрізь. - JSON Schema Core, draft 2020-12 — у JSON Schema пропуск
additionalPropertiesдорівнює порожній схемі — зайве проходить і лишається.
- Playwright — class APIRequestContext — у запитів є дефолтний таймаут, який знімається лише
timeout: 0;maxRetriesза замовчуванням0, повторює тількиECONNRESETі не дивиться ні на метод, ні на код відповіді. - MDN — Idempotent (Glossary) — що безпечно повторювати: означає однаковий задуманий ефект на сервері, і
POSTдо неї не належить. - Playwright — Test retries — ретрай на рівні ранера — це повторний прогін тесту цілком; він дає сигнал про нестабільність, а не лікує причину.
- Playwright — Best Practices — кожен тест має бути повністю ізольований і виконуватися незалежно — з власними даними й станом.
- xUnit Test Patterns — Erratic Test — маркер прихованої залежності: тест сам по собі зелений, а в сюїті падає від додавання або вилучення іншого; причина зазвичай — спільна фікстура.
- Martin Fowler — Eradicating Non-Determinism in Tests — ізоляція проти явно оголошених залежностей — дві школи; джерело віддає перевагу ізоляції за гнучкість.
- MDN — Using the Fetch API —
fetchвиконує проміс і на404: перевіряти требаstatus/ok, а не сподіватися наcatch. - JSON Schema Core, draft 2020-12 — без явної заборони зайві поля проходять валідацію мовчки.
- Zod — API (Objects) — у Zod дефолтний
stripприбере зайве поле сам — перевірка «поля немає» буде хибно зеленою. - Martin Fowler — Eradicating Non-Determinism in Tests — тест, який зеленіє з другого разу, лишається зламаним: повтор ховає недетермінованість, а не усуває її.
Відео до теми
1. Start of API testing projectросHOT testing
2. ControllersросHOT testing
3. Request BuilderросHOT testing
4. Types for request and responseросHOT testing
6. Client: Handling authentication in testsросHOT testing
перший API тест з playwrightqa senpai | test automation
автоматизація тестуваня API з playwrightqa senpai | test automation
1. Мігруемо API тести з MochaJS на Playwright testHOT testing
2. Переводимо setup скріпт на PlaywrightHOT testing
3. Переписуемо відправку реквестів на PlaywrightHOT testing
будівельник - дизайн патерн для автоматизації API | builder test automation patternqa senpai | test automationНавіщо переносити API-тести з Postman/Newman у код?
Колекція чудово працює, поки запитів десятки чи сотня, але погано масштабується під тисячі перевірок у CI. У коді ти отримуєш той самий мовний стек, репозиторій і рев'ю-процес, що й у продукту: авторизації, спільні , читабельні падіння в , нормальна робота з й код-рев'ю. Postman лишається зручним для ручного дослідження й дебагу окремого запиту, а систематичну на кожен PR тримає код. Практичний тригер переходу — момент, коли колекцію вже страшно чіпати, бо її скрипти сплелися між собою.
Якими HTTP-клієнтами ходять в API з коду в JS/TS і чим вони відрізняються?
HTTP-клієнт (HTTP client) — це бібліотека, що відправляє запит і повертає відповідь як об'єкт. У JS/TS реально зустрічаються чотири: fetch (вбудований у браузер і Node.js з 18-ї версії), axios (зовнішня бібліотека), APIRequestContext (з Playwright) і supertest (для тестування свого сервера в процесі). Головна вісь відмінності — поведінка на HTTP-помилку: fetch і APIRequestContext не кидають на 4xx/5xx (у Playwright це дефолт із перемикачем failOnStatusCode), axios за замовчуванням кидає, supertest асертиться окремо. Вибір клієнта визначає, як ти обробляєш помилки, й серіалізацію, тож це не питання смаку.
Чому fetch не впав на статусі 500?
Бо fetch не вважає HTTP-помилку помилкою виконання запиту. він відхиляє лише на частині умов — MDN називає серед них мережевий збій і невідповідну схему URL, а ще додає AbortError на скасування через AbortController; перелік у джерелі відкритий. А відповідь із 404 чи 500 для fetch — запит, що вдався: проміс резолвиться нормальним обʼєктом відповіді, лише response.ok буде false, тож catch навколо нього не спрацює. Тому статус треба перевіряти явним (expect(res.status()).toBe(...)), а не сподіватись, що try/catch зловить 5xx. Хто плутає це, той писав тільки .
Чим axios поводиться інакше за fetch на помилку?
axios за замовчуванням відхиляє проміс на будь-який статус поза діапазоном 2xx — тобто робить те, чого не робить fetch. Це регулюється опцією validateStatus. Для продакшн-коду зручно (помилка одразу летить у catch), але для тестів радше заважає: ти й так хочеш перевіряти статус явним асертом, а не ловити його через try/catch. Тому в тестах на axios часто налаштовують validateStatus так, щоб він не кидав, і асертять статус вручну.
Що таке APIRequestContext і чим він зручний для API-тестів?
Це вбудований HTTP-клієнт Playwright, доступний у тестах як request. Поводиться він як fetch — HTTP-помилка не кидає виняток, її треба асертити; це дефолт із перемикачем failOnStatusCode, який вмикає кидання на коди поза 2xx і 3xx. Головна вигода — інтеграція з рештою Playwright: свій baseURL, усі виклики видно в трейсі, а варіанти page.request/context.request ділять cookie з браузерним контекстом — залогінився через API, перевіряєш в UI (сама фікстура request ізольована й cookie сторінки не бачить). Якщо стек уже на Playwright, це природний вибір для API- і підготовки стану поруч з e2e.
Навіщо supertest і коли він не потрібен?
supertest призначений тестувати HTTP-сервер у тому ж процесі: ти передаєш йому свій застосунок (наприклад, Express), він піднімає його на ефемерному порту й ходить у нього без реальної мережі. Це швидко й ізольовано, тому підходить для компонентних тестів бекенду зсередини. Але працює лише коли сервер під рукою в коді. Для тестування задеплоєного API по URL (стейдж, прод) він не потрібен — там ходять APIRequestContext, axios чи fetch.
Що таке arrange–act–assert і як він читається для API-тесту?
Arrange–act–assert (AAA), «підготуй — дій — перевір» — канонічна структура тесту, однакова для UI та API. Arrange — попередні умови сцени: логін і токен, насіяний через API ресурс, потрібний стан бази — усе, чого головний запит потребує, але що саме по собі не перевіряється. Act — рівно один запит, поведінку якого перевіряє тест. Assert — перевірки відповіді: статус, тіло, заголовки, відповідність схемі. Розділення фаз робить тест читабельним і показує, що саме тестується.
Чому в тесті має бути рівно один головний запит (act)?
Бо якщо в тесті два «головні» запити, поведінку яких ти перевіряєш, — це насправді два тести, злиті в один. Коли такий тест червоніє, незрозуміло, який саме крок зламався, і звіт бреше. Запити на підготовку стану (створити ресурс, залогінитись) не рахуються за act — це arrange. Один act на тест також спрощує назву: вона описує одну конкретну перевірку («повертає 201 зі схемою Order»), а не ланцюжок дій.
Що таке патерн API-клієнта і яку проблему він вирішує?
Патерн API-клієнта (API client) — це клас, який ховає за методом з людською назвою: замість сирого request.post('/orders', { headers: ... }) у кожному тесті — orders.createOrder({ productId, qty }). Проблема, яку він розв'язує, та сама, що й для UI: без нього базовий URL, заголовок авторизації й розбір відповіді дублюються в кожному тесті, і зміна формату авторизації вимагає правок у ста місцях. Клієнт робить це єдиним джерелом істини, а тест читається як сценарій, а не як HTTP-механіка.
Чому не варто класти асерти всередину API-клієнта?
Бо клієнт із перевірками всередині перетворюється на : він змішує транспорт (як сходити в ендпоінт) з перевіркою (що вважати правильним) і ховає, що саме асертиться в конкретному тесті. Клієнт має віддавати «сиру» відповідь, а перевірки лишатись у тесті — тоді один і той самий метод createOrder годиться і для позитивного тесту (очікуємо 201), і для негативного (очікуємо 422). Це та сама дисципліна розділення відповідальностей, що й «локатори в Page Object, асерти в тесті».
Що таке ланцюжки (chaining) в API-тестах?
Ланцюжок — це коли відповідь одного запиту дає вхід для наступного. Частина перевірок неможлива без попереднього стану: GET /orders/{id} нема з чим викликати, поки інший запит не створив замовлення й не віддав його id. Класичний ланцюжок: POST /orders віддає id, який ти підставляєш у POST /orders/{id}/cancel, а потім перевіряєш GET /orders/{id}. Частина ланцюжка — це arrange (підготовка стану), і лише один крок — справжній act, який ти перевіряєш.
Чому тестові дані для API краще готувати через API, а не через UI?
Бо API- (seeding) на порядок швидший і стабільніший за клікання майстром у браузері. Якщо тесту потрібне замовлення, клацати його через UI означає протягнути в arrange весь фронтенд з його рендером, анімаціями й — і все це заради декорацій, а не заради самої перевірки. Головне правило: готуй стан найдешевшим надійним способом. Відповідь «створюю руками в базі» на співбесіді насторожує (крихко, обходить бізнес-логіку), а «сідінг через API» — сильний сигнал.
Що таке teardown і чому його вішають на after-хук?
Teardown () — це видалення того, що тест насіяв: створені замовлення, юзери, файли. Без нього середовище засмічується, і сусідні прогони починають бачити чужі дані, а на повторному запуску вилазять конфлікти (409 на зайнятий email). Прибирання вішають на after-хук (afterEach/finally), щоб воно відпрацювало навіть коли основна перевірка впала посередині: інакше кожен червоний тест лишав би за собою сміття. Але сам after-хук — відоме місце прихованих збоїв: виняток під час прибирання лишає тест зеленим, а ізоляцію для наступних тестів ламає. Це та сама пара «створив — прибрав», що й у будь-якому керуванні ресурсами.
Що таке eventual consistency і чому sleep — погане лікування?
Eventual consistency () — властивість розподілених систем, коли створений ресурс не завжди видно наступним запитом миттєво: репліка ще не наздогнала майстер. Симптом — «баг у продукті, ресурс не з'явився», а насправді наступний запит прийшов раніше, ніж дані узгодились. sleep(3000) — погане лікування одразу з двох причин: на швидкому середовищі він марно гальмує прогін, а на повільному все одно не встигне й тест . Правильно — (polling) з таймаутом: періодично перепитувати until-умову, поки не справдиться або не вийде час (expect.poll у Playwright).
Навіщо валідувати схему відповіді і чим ajv відрізняється від zod?
Перевіряти тіло пополе (expect(body.id).toBeDefined()) виснажливо й дірчасто: забув поле — пропустив регресію. Валідація проти схеми перевіряє всю форму відповіді одним асертом. ajv — валідатор JSON Schema: описуєш структуру мовою JSON Schema, ajv компілює її у швидку функцію й повертає розбіжності; головна вигода — схему можна взяти прямо з OpenAPI-специфікації, перетворивши доку на тест. zod — TS-first: схему описуєш кодом на TypeScript, parse перевіряє в рантаймі, а бонус — з тієї ж схеми виводиться статичний тип. Груба евристика: є OpenAPI й потрібна одна правда для доки й тестів — ajv; пишеш на TS з нуля й хочеш статичні типи — zod.
Чому у схемі важливо забороняти зайві поля?
Бо за замовчуванням валідатор перевіряє лише те, що ти описав, і пропускає все зверху. Якщо бекенд почне повертати нове несподіване поле (наприклад, витік внутрішнього passwordHash чи internalNote), м'яка схема цього не помітить — регресія проскочить. Тому за замовчуванням зайві поля забороняють: у JSON Schema це additionalProperties: false, у Zod — z.strictObject() (методи .strict()/.passthrough() задепрекейчено). Без заборони інструменти поводяться по-різному: JSON Schema зайве поле пропускає, а Zod вирізає його з результату розбору — обидва варіанти однаково ховають регресію. Із забороною будь-яка структурна зміна відповіді — і зникнення поля, і поява зайвого — валить тест дешево, одним асертом.
Навіщо таймаут у тесті і чим загрожують занизьке та зависоке значення?
Таймаут (timeout) — це межа очікування відповіді, після якої запит вважається провальним. Без нього завислий запит тримає весь прогін, аж поки не спрацює глобальний ліміт з малоінформативною помилкою. Тому межу задають свідомо на двох рівнях: для окремого запиту в клієнті й для тесту цілком. Занизьке значення породжує флак на повільному CI (нормальна відповідь просто не встигає прийти), зависоке — ховає деградацію продуктивності (сервіс уже відповідає вдвічі повільніше, а тест мовчить). Значення обирають з голови сервісу, а не наосліп.
Ретраї в тестах — це добре чи погано?
Залежить від того, що саме повторюють — тут перевіряють зрілість. Треба розрізняти три речі. Мережевий ідемпотентного запиту (повторити GET, що впав через скидання з'єднання) — безпечно, бо повтор не змінює стан сервера; а от наосліп повторювати POST, що щось створює, — дубля (рятує Idempotency-Key, якщо API його підтримує). Інструмент про це правило не знає: maxRetries у Playwright повторює лише ECONNRESET і робить це без огляду на метод. Полінг для eventual consistency — це не ретрай, а очікування стану з таймаутом, легітимний патерн. Ретрай усього тесту, що впав — червоний : тест, який проходить лише з другої спроби, лишився зламаним — повтор прибрав симптом зі звіту, а не причину. Одним рядком: таймаут і полінг лікують чесну асинхронність, сліпий ретрай усього тесту мовчки ховає баг.
Що потрібно, щоб запускати API-тести паралельно?
Ключове слово — ізоляція даних (data isolation); є й конкурентна школа, що робить ставку на явно визначені залежності й заданий порядок тестів, але канон цієї глави — ізоляція, бо вона дає більше гнучкості. Без браузера й рендеру API-тести дешеві, тому їх запускають на десятках — і прогін скорочується з десятків хвилин до одиниць. Але паралельність безжально карає за спільний стан: два воркери, що правлять один ресурс, дають гонку й «іноді падає» без видимої причини. Тому кожен тест має працювати зі своїми даними: згенерований унікальний email, власне замовлення, а в ідеалі — окремий акаунт чи tenant на воркер. Антипатерн, згадка якого — сильний сигнал: один спільний тестовий юзер на всю сюїту, за стан якого тести стають у чергу.
Чому один спільний тестовий юзер — антипатерн для паралельного запуску?
Бо щойно тести стартують паралельно, вони починають перетирати одне одному стан цього юзера: один змінює його кошик, другий у цей момент його читає й бачить чуже. Виникає прихована залежність через дані, яка маскується під незалежні тести, — вона працює послідовно й розсипається в паралелі. Лікування — ізоляція: генерувати унікальні дані на кожен тест або виділяти окремий акаунт/tenant на воркер. Окрема пастка того ж роду: глобальний лічильник, послідовні id чи спільний кошик в API — такі місця «стріляють» першими, тож їх закладають у дизайн даних одразу.
Три кейси, які проходять шлях від сирого запиту до інженерної : винести в API-клієнт і написати чистий arrange–act–assert, зібрати ланцюжок з підготовкою стану, і замість sleep, і замкнути перевірку тіла на схему з забороною зайвих полів. Скрізь — що дивитися і чому.
Кейс 1. API-клієнт і чистий arrange–act–assert
Сирий запит у кожному тесті розповзається копіпастою: заголовок авторизації, базовий шлях і розбір відповіді доводиться правити в десятках місць. Виносимо ендпоінти в клас-клієнт, а тест лишаємо про перевірку.
// orders-api.ts — словник ендпоінтів, жодних асертів усередині
import { APIRequestContext } from '@playwright/test';
type OrderInput = { productId: number; qty: number };
export class OrdersApi {
constructor(private request: APIRequestContext, private token: string) {}
createOrder(data: OrderInput) {
return this.request.post('/orders', {
headers: { Authorization: `Bearer ${this.token}` },
data,
});
}
getOrder(id: string) {
return this.request.get(`/orders/${id}`, {
headers: { Authorization: `Bearer ${this.token}` },
});
}
cancelOrder(id: string) {
return this.request.post(`/orders/${id}/cancel`, {
headers: { Authorization: `Bearer ${this.token}` },
});
}
deleteOrder(id: string) {
return this.request.delete(`/orders/${id}`, {
headers: { Authorization: `Bearer ${this.token}` },
});
}
}
// orders.spec.ts — тест читається як сценарій, а не як HTTP-механіка
import { test, expect } from '@playwright/test';
import { OrdersApi } from './orders-api';
test('POST /orders створює замовлення й повертає 201 зі статусом created', async ({ request }) => {
// Arrange — декорації сцени, не предмет перевірки
const token = await auth.loginAs('customer');
const orders = new OrdersApi(request, token);
// Act — рівно ОДИН головний запит
const res = await orders.createOrder({ productId: 42, qty: 2 });
// Assert — статус і форма відповіді
expect(res.status()).toBe(201);
const body = await res.json();
expect(body).toMatchObject({ productId: 42, qty: 2, status: 'created' });
});
Що дивитися і чому:
- лишились у тесті, клієнт віддає сиру відповідь. Той самий
createOrderгодиться і для позитивного тесту (чекаємо201), і для негативного (чекаємо422на биті дані). Занеси перевірку в клієнт — і він стане , який ховає, що саме тестується. - Один act на тест.
loginAsіnew OrdersApi— це arrange, а не другий головний запит. Якби в тесті було два перевірюваних виклики, червоний звіт не сказав би, який саме зламався. - Назва описує перевірку, а не дію. «Повертає 201 зі статусом created» у червоному звіті вже розповідає, що зламалось; «тест POST orders» — ні.
res.status()— явний асерт, а неtry/catch.APIRequestContextне кидає на4xx/5xx, тож статус перевіряється руками. Наївнийcatchтут не спрацював би на500.
Кейс 2. Ланцюжок: підготовка стану, прибирання, полінг
Скасування замовлення не перевірити «на порожньому місці» — замовлення спершу треба створити. Це ланцюжок: id зі створеного ресурсу йде у наступні виклики. Стан сіємо через API (не через UI), прибираємо в after-хуці, а на eventual consistency ставимо полінг замість паузи навмання.
import { test, expect } from '@playwright/test';
import { OrdersApi } from './orders-api';
test.describe('скасування замовлення', () => {
let orders: OrdersApi;
let orderId: string;
test.beforeEach(async ({ request }) => {
const token = await auth.loginAs('customer');
orders = new OrdersApi(request, token);
// Arrange — сідж стану найдешевшим надійним способом: запитом, не кліком
const created = await orders.createOrder({ productId: 42, qty: 2 });
expect(created.status()).toBe(201);
orderId = (await created.json()).id;
});
test.afterEach(async () => {
// Teardown на after-хуці — відпрацює навіть коли перевірка впала
await orders.deleteOrder(orderId).catch(() => {});
});
test('POST /orders/{id}/cancel переводить замовлення у cancelled', async () => {
// Act — те, що справді перевіряємо
const res = await orders.cancelOrder(orderId);
expect(res.status()).toBe(200);
// Assert зі стійкістю до eventual consistency: полінг, а не sleep(3000)
await expect.poll(async () => {
const got = await orders.getOrder(orderId);
return (await got.json()).status;
}, { timeout: 5000 }).toBe('cancelled');
});
});
Що дивитися і чому:
- через API, а не через UI. Замовлення в
beforeEachстворюється запитом за мілісекунди й без фронтенду. Клацати його майстром у браузері означало б протягнути в arrange весь рендер заради декорацій. - Teardown у
afterEach, а не в кінці тесту. Якби прибирання стояло останнім рядком тесту, воно б не відпрацювало на падінніcancel, і середовище засмічувалось би..catch(() => {})глушить помилку прибирання, щоб вона не маскувала справжню причину падіння — але ціна цього рішення відома: заглушений виняток у прибиранні лишає тест зеленим, а ізоляцію наступним ламає, тож такі місця варто логувати. expect.pollзамістьsleep. Післяcancelрепліка може ще не наздогнати, і миттєвийGETвіддасть старий статус. Полінг перепитує до 5 секунд і зеленіє щойно узгодилось — на швидкому середовищі не гальмує, на повільному не .sleep(3000)програє в обидва боки.- Ланцюжок ≠ один тест. Створення в
beforeEach— arrange,cancel— act,getOrder— assert. Перевіряється рівно один перехід стану.
Кейс 3. Валідація тіла проти схеми з забороною зайвих полів
Перевіряти тіло пополе (expect(body.id).toBeDefined()) дірчасто: забув поле — пропустив , а зайве поле взагалі не помітиш. Замикаємо всю форму на одну схему Zod і забороняємо зайве через z.strictObject().
import { test, expect } from '@playwright/test';
import { z } from 'zod';
import { OrdersApi } from './orders-api';
// strict-обʼєкт → будь-яке несподіване поле у відповіді провалить parse
const Order = z.strictObject({
id: z.string().uuid(),
productId: z.number().int(),
qty: z.number().int().positive(),
status: z.enum(['created', 'paid', 'cancelled']),
});
test('GET /orders/{id} повертає тіло, що точно відповідає схемі Order', async ({ request }) => {
const token = await auth.loginAs('customer');
const orders = new OrdersApi(request, token);
const { id } = await (await orders.createOrder({ productId: 42, qty: 2 })).json();
const res = await orders.getOrder(id);
expect(res.status()).toBe(200);
const body = await res.json();
const order = Order.parse(body); // кине, якщо форма не збіглась; далі order типізований
expect(order.status).toBe('created');
});
Що дивитися і чому:
- Одна схема замість десятка
expect.Order.parseперевіряє всю форму разом: типи, набір полів, допустимі значенняstatus. Зникло поле чи змінився тип —parseкине з описом розбіжності, і жодна регресія не проскочить. - Strict-обʼєкт ловить зайве поле. Без нього відповідь із витеклим
internalNoteчиpasswordHashне провалила б валідацію: Zod за замовчуванням мовчки вирізає нерозпізнані ключі з результату, а JSON Schema безadditionalProperties: falseпросто їх пропускає. - Бонус типізації. Після
parseзміннаorderтипізована — далі працюєш без ручнихas, а редактор підказує поля. Це TS-first перевагаzod. - Коли натомість
ajv. Якщо схеми вже описані в OpenAPI й хочеться однієї правди для доки й тестів — беруть JSON Schema прямо зі специфікації й валідуютьajv, фактично перетворюючи доку на тест.zod— коли тести пишуться на TS з нуля й потрібні статичні типи.
HTTP-клієнти
- Знаю чотири клієнти в JS/TS (
fetch,axios,APIRequestContext,supertest) і чим вони відрізняються за поведінкою на помилку та контекстом застосування. - Можу пояснити, чому
fetchіAPIRequestContextне кидають на4xx/5xx:500— це успішний запит зresponse.ok === false, тож статус перевіряють ; аfetchвідхиляється лише на частині умов (мережевий збій, крива схема URL, скасування черезAbortController) — перелік у джерелі відкритий. - Знаю, що
axiosза замовчуванням кидає на будь-який статус поза2xx(регулюєтьсяvalidateStatus), аsupertestасертить статус окремо. - Розумію, що
APIRequestContextдаєbaseURL, запис у , а черезpage.request/context.request— спільні cookie з браузером (requestізольована), тоді якsupertestпотрібен для свого сервера в процесі (ефемерний порт), не для задеплоєного API по URL.
Структура тесту (arrange–act–assert)
- Знаю три фази AAA і що для API arrange — це підготовка стану, act — рівно один головний запит, assert — перевірки відповіді.
- Розумію, чому в тесті один act (два «головні» запити — це два тести, і червоний звіт інакше бреше) і чому назва описує перевірку («повертає 201 зі схемою Order»), а не дію («тест POST orders»).
- Знаю, що тест не має залежати від порядку й сам готує свій стан — це паралельного запуску.
Патерн API-клієнта
- Можу пояснити, яку проблему розв'язує патерн API-клієнта: ховає ,
baseURLі авторизацію за методом з людською назвою, як для UI; розподіл шарів — HTTP-клієнт про транспорт, API-клієнт про словник ендпоінтів, тест лише про перевірку. - Розумію, чому асерти не кладуть у клієнт: інакше він стає , змішує транспорт з перевіркою і ховає, що саме тестується.
Ланцюжки й підготовка стану
- Знаю, що таке ланцюжок (chaining): відповідь одного запиту (
idствореного ресурсу) дає вхід для наступного. - Можу пояснити, чому стан готують через API-, а не через UI (швидше, стабільніше, не додає до сценарію, що про інше), чому teardown вішають на after-хук — щоб відпрацювало навіть коли основна перевірка впала — і чому за самим прибиранням треба стежити: виняток у ньому лишає тест зеленим, а ламає ізоляцію наступним.
- Знаю різницю між справжнім багом «ресурс не з'явився» та eventual consistency, коли репліка ще не наздогнала, і чому її лікують із , а не
sleep(3000).
Валідація схем
- Розумію, чому перевірка всієї форми проти схеми надійніша за перевірку кількох полів пополе.
- Знаю різницю
ajvvszod:ajv— JSON Schema (можна взяти з OpenAPI),zod— TS-first зі статичним типом з тієї ж схеми. - Можу пояснити, чому за замовчуванням забороняють зайві поля (
additionalProperties: falseу JSON Schema,z.strictObject()у Zod), і знаю, що без заборони поводяться вони по-різному: JSON Schema поле пропускає, Zod — вирізає з результату.
Таймаути й ретраї
- Розумію, навіщо таймаут (не підвісити прогін) і чому занизьке значення дає , а зависоке ховає деградацію продуктивності.
- Знаю різницю між трьома «повторами»: мережевий ідемпотентного запиту (наосліп
POST— дубля, рятуєIdempotency-Key), полінг стану й ретрай усього тесту — і щоmaxRetriesу Playwright повторюєECONNRESETбез огляду на метод. - Розумію, чому ретрай усього тесту — червоний : «зеленіє з другого разу» ховає флак, а не лікує причину.
Паралельний запуск
- Знаю, що головна передумова безпечної паралельності — ізоляція даних (альтернативна школа — явні залежності й заданий порядок), чому спільний стан дає гонки й чому один спільний тестовий юзер — антипатерн прихованої залежності через дані.
- Розумію, чому глобальні лічильники, послідовні
idчи спільний кошик «стріляють» першими в паралелі.
Квіз
Перед стартом
- Питань: 14
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Чому API-тести переїжджають з Postman/Newman у код?