REST API та формати даних
Зміст
Веб-інтерфейс, який ви тестуєте очима, — це лише вершина. Під кнопками й формами живе інший, значно швидший і стабільніший світ: HTTP-запити до серверного API. Розуміти цей світ для AQA не опція, а базова грамотність. API-тести менш крихкі за UI, дають точний контроль над станом системи й дозволяють підготувати дані для сценарію за мілісекунди замість кліків через півсторінки. Цей розділ пояснює, як влаштований найпоширеніший стиль веб-API — REST, — і що з ним робити в автотестах.
Що таке REST і навіщо він потрібен
REST (Representational State Transfer) — це не протокол і не стандарт, а архітектурний стиль, який Рой Філдінг (Roy Fielding) описав у своїй докторській дисертації 2000 року. Слово «стиль» тут ключове: REST не змушує використовувати конкретні заголовки чи формати, він задає набір обмежень (constraints), і система, що їх дотримується, вважається «RESTful».
Обмеження такі:
- Клієнт-сервер (client-server). Клієнт (браузер, мобільний застосунок, ваш автотест) і сервер розділені. Вони спілкуються лише через чітко визначений інтерфейс, тож можуть розвиватися незалежно.
- Відсутність стану (statelessness). Сервер не памʼятає нічого між запитами. Кожен запит містить усе потрібне для його обробки — токен авторизації, параметри, тіло. Це прямо впливає на тести: не існує «сесії», яку треба вибудовувати клацаннями; кожен виклик самодостатній, а отже, його легко повторити чи запустити ізольовано.
- Кешованість (cacheability). Відповіді можуть позначатися як кешовані або ні, щоб клієнти й проміжні вузли не смикали сервер зайвий раз.
- Однаковий інтерфейс (uniform interface). Ресурси адресуються однотипно (через URL), маніпуляції над ними — стандартними HTTP-методами, а повідомлення самоописові. Сюди ж належить HATEOAS (Hypermedia as the Engine of Application State) — ідея, що відповідь несе посилання на доступні далі дії, тож клієнт «мандрує» API за наданими лінками, а не хардкодить URL. На практиці HATEOAS реалізують рідко, і більшість «REST API» його ігнорують.
- Багатошаровість (layered system). Між клієнтом і сервером можуть стояти проксі, балансувальники, шлюзи — клієнт про них не знає.
- Код на вимогу (code on demand) — єдине необовʼязкове обмеження: сервер може віддавати виконуваний код (наприклад, JavaScript).
Практичний висновок для тестувальника: якщо API справді дотримується цих обмежень, ви можете розраховувати на передбачувану поведінку — однакові URL, однакові методи, самодостатні запити. На практиці багато «REST API» є RESTful лише частково, і це нормально: терміном часто називають будь-який HTTP-API з JSON-відповідями.
Ресурс і ендпоінт
Ресурс (resource) — це будь-яка сутність, якою оперує система: користувач, замовлення, тест-ран, файл. У REST мислять іменниками, а не дієсловами. Немає ендпоінта «створити користувача»; є ресурс «користувачі», над яким виконують дію методом.
Ендпоінт (endpoint) — конкретна URL-адреса, за якою доступний ресурс або колекція ресурсів. Порівняйте:
| URL | Що це |
|---|---|
/api/v1/users | колекція всіх користувачів |
/api/v1/users/42 | конкретний користувач з id 42 |
/api/v1/users/42/orders | замовлення користувача 42 (вкладений ресурс) |
/api/v1/orders?status=paid | колекція замовлень із фільтром через query-параметр |
Зверніть увагу на конвенції: колекції — множина, ідентифікатор іде окремим сегментом шляху, а фільтри/сортування/пагінація — через параметри рядка запиту (query string) після ?. Гарний REST-URL не містить дієслів (/getUser, /createOrder — антипатерн): дію задає HTTP-метод.
HTTP-методи й відповідність CRUD
CRUD — це чотири базові операції над даними: Create, Read, Update, Delete. REST відображає їх на HTTP-методи так:
| Операція | Метод | Приклад | Безпечний (safe)? | Ідемпотентний (idempotent)? |
|---|---|---|---|---|
| Read | GET | GET /users/42 | так | так |
| Create | POST | POST /users | ні | ні |
| Update (повна заміна) | PUT | PUT /users/42 | ні | так |
| Update (часткова) | PATCH | PATCH /users/42 | ні | не обовʼязково |
| Delete | DELETE | DELETE /users/42 | ні | так |
Ще є HEAD (як GET, але без тіла — лише заголовки) та OPTIONS (запит про можливості ресурсу, використовується в CORS-preflight).
Два поняття з таблиці варто розкрити, бо вони прямо стосуються надійності тестів.
Безпечний метод (safe) — той, що не змінює стан на сервері. GET лише читає, тож у тесті GET-запити можна повторювати скільки завгодно без побічних ефектів.
Ідемпотентний метод (idempotent) — той, який при повторному виклику з тими самими даними дає той самий кінцевий стан. DELETE /users/42, викликаний двічі, залишає систему в тому ж стані: користувача немає (хоча статус-код другого виклику може бути іншим — про це нижче). PUT замінює ресурс цілком, тож повтор не додає нічого нового. А POST не ідемпотентний: два POST /users створять двох користувачів.
Точні визначення safe та idempotent для кожного методу закріплені в HTTP-специфікації (RFC 9110 «HTTP Semantics»). При написанні контрактних тестів звіряйтеся з нею для крайових випадків — зокрема поведінки PATCH, ідемпотентність якого залежить від семантики самої зміни.
JSON: типи даних
JSON (JavaScript Object Notation) — домінантний формат обміну в сучасних REST API. Він текстовий, читабельний і має шість типів значень: чотири прості (рядок, число, булеве значення, null) і два структурні (обʼєкт, масив).
{
"string": "рядок у подвійних лапках",
"number": 42.5,
"boolean": true,
"nothing": null,
"object": { "key": "value" },
"array": [1, 2, 3]
}
Кілька особливостей, які регулярно спотикають початківців:
- Числа не поділяються на цілі й дробові. У JSON є один тип
number— жодних окремихintчиfloat. Це важливо для перевірок: не покладайтеся сліпо на те, що поле прийде саме цілим — залежить від того, як його серіалізував бекенд. - Немає типу «дата». Дати передають рядком (найчастіше у форматі ISO 8601, наприклад
"2026-07-08T10:15:30Z") або числом (Unix-час). Формат — частина контракту, і його треба перевіряти явно. - Ключі обʼєктів — завжди рядки в подвійних лапках. Одинарні лапки недопустимі.
- Немає коментарів. Коментар у файлі, що претендує на JSON, зробить його невалідним.
null— це значення, а не відсутність поля. Розрізняйте{"phone": null}(поле є, значення порожнє) і{}(поля немає взагалі) — для тестів це різні контракти.
Обʼєкт vs масив і вкладеність
Дві структури-контейнери JSON плутають найчастіше. Обʼєкт (object) — невпорядкований набір пар «ключ-значення» у фігурних дужках {}. Масив (array) — впорядкований список значень у квадратних [].
Практичне правило: якщо ви звертаєтесь до елемента за назвою — це обʼєкт (user.email); якщо за позицією — масив (users[0]). Реальні відповіді комбінують обидві структури й бувають глибоко вкладеними:
{
"id": 42,
"name": "Alice",
"roles": ["admin", "qa"],
"profile": {
"city": "Kyiv",
"contacts": [
{ "type": "email", "value": "alice@example.com" },
{ "type": "phone", "value": "+380..." }
]
}
}
Тут roles — масив рядків, profile — вкладений обʼєкт, а profile.contacts — масив обʼєктів. Шлях до першого email виглядатиме як profile.contacts[0].value. Уміння впевнено читати такі шляхи — щоденний навик при написанні асертів і при роботі з JSONPath у тестових інструментах.
Важлива дрібниця для стабільності тестів: порядок ключів в обʼєкті не гарантований, а порядок елементів масиву — значущий. Тому не пишіть асерт, що покладається на послідовність ключів обʼєкта; а якщо API повертає масив без явного сортування, порядок його елементів може «плавати» від запиту до запиту й давати флак (flakiness). У такому разі сортуйте колекцію в тесті перед порівнянням або перевіряйте наявність елементів без прив’язки до позиції.
Серіалізація й десеріалізація
Серіалізація (serialization) — перетворення обʼєкта в памʼяті (структури мови програмування) на текст для передавання мережею. Десеріалізація (deserialization) — зворотний процес: текст перетворюється на обʼєкт, з яким зручно працювати в коді.
У JavaScript це два вбудованих методи:
const user = { id: 42, name: "Alice" };
const text = JSON.stringify(user); // серіалізація → '{"id":42,"name":"Alice"}'
const back = JSON.parse(text); // десеріалізація → знову обʼєкт
Чому це не абстрактна теорія для AQA: майже кожен API-тест виконує десеріалізацію тіла відповіді, щоб дістатися до полів. Тут ховаються типові помилки. JSON.parse кине виняток на невалідному JSON — а порожнє тіло, HTML-сторінка помилки замість JSON чи обірваний потік саме такими і є. Тому надійний тест спершу перевіряє Content-Type та статус, і лише потім парсить тіло — інакше падіння на парсингу замаскує справжню причину. Ще нюанс: при серіалізації JavaScript перетворює Date на рядок ISO 8601, а undefined-поля просто зникають — про це варто памʼятати, готуючи тіло запиту.
JSON проти XML
До панування JSON у вебсервісах був XML (Extensible Markup Language), і ви досі зустрічатимете його в legacy-системах, SOAP-сервісах та деяких банківських/урядових інтеграціях.
<user id="42">
<name>Alice</name>
<roles>
<role>admin</role>
<role>qa</role>
</roles>
</user>
| Критерій | JSON | XML |
|---|---|---|
| Багатослівність | компактний | більш громіздкий (закривні теги) |
| Типи даних | вбудовані (number, boolean, null…) | усе текст, типи задає схема |
| Атрибути | немає, лише пари ключ-значення | є атрибути + вміст тегів |
| Схема/валідація | JSON Schema | XSD, DTD |
| Коментарі | немає | є |
| Простори імен | немає | є (namespaces) |
| Пошук по документу | JSONPath | XPath |
| Типове застосування | REST, сучасні веб-API | SOAP, legacy, документо-орієнтовані формати |
Для тестувальника головна відмінність практична: XML має атрибути й простори імен, тож той самий «шлях до значення» в XML описується через XPath, а в JSON — через крапкову нотацію або JSONPath. Якщо тестуєте XML-API, готуйтеся працювати з XPath-локаторами по відповіді — логіка та сама, що й у DOM.
application/x-www-form-urlencoded проти JSON-тіла
Коли клієнт надсилає дані на сервер (POST, PUT, PATCH), формат тіла оголошується заголовком Content-Type. Два поширені варіанти для AQA — form-urlencoded і JSON.
application/x-www-form-urlencoded — формат, у якому дані кодуються як пари ключ=значення, зʼєднані &, зі спецсимволами у percent-encoding. Пробіл у цьому форматі кодується символом + (декодери також приймають %20). Це те, що історично надсилає HTML-форма з method="post" без явного enctype.
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=secret%21
application/json — тіло є JSON-документом, здатним передати вкладеність і типи:
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{ "name": "Alice", "roles": ["admin", "qa"] }
| form-urlencoded | JSON | |
|---|---|---|
| Структура | плоскі пари ключ-значення | вкладені обʼєкти й масиви |
| Типи | усе рядки | number, boolean, null, обʼєкти, масиви |
| Спецсимволи | percent-encoding (пробіл — +) | екранування в лапках |
| Типове застосування | форми логіну, прості дії, OAuth token-запити | сучасні REST-API |
Головний висновок: Content-Type має відповідати реальному тілу. Класична причина, чому «в браузері працює, а в тесті ні», — тест надсилає JSON-тіло, але сервер очікує form-urlencoded (або навпаки), і повертає 400 чи 415 (Unsupported Media Type). Перш ніж воювати з тестом, підгляньте справжній запит у DevTools (вкладка Network) і скопіюйте Content-Type та формат тіла точно.
Чому в тесті перевіряють і статус, і тіло
Спокуса перевірити лише статус-код («прийшло 200 — значить ок») — джерело хибно-зелених тестів. Статус і тіло відповідають на різні запитання:
- Статус-код (status code) каже, як сервер поставився до запиту на рівні протоколу: прийняв, відхилив, не знайшов, впав.
- Тіло (body) каже, що саме повернулося: ті дані, ті значення, та структура.
Приклади, чому обох потрібно разом:
- Сервер повертає
200 OK, але тіло —{"error": "not found"}замість очікуваного обʼєкта (антипатерн, який трапляється частіше, ніж хотілось би). - Створення повернуло
201 Created, але полеidу тілі —null, тож наступний крок тесту не матиме з чим працювати. - Оновлення повернуло
200, тіло теж схоже на правильне, але змінилося не те поле.
Мінімальний надійний асерт перевіряє три речі: статус, Content-Type і структуру/значення тіла.
const res = await request.post('/api/v1/users', {
data: { name: 'Alice', email: 'alice@example.com' },
});
expect(res.status()).toBe(201);
expect(res.headers()['content-type']).toContain('application/json');
const body = await res.json();
expect(body).toMatchObject({
id: expect.any(Number),
name: 'Alice',
email: 'alice@example.com',
});
Зверніть увагу на expect.any(Number): ми фіксуємо тип і наявність поля, не привʼязуючись до конкретного згенерованого id. Це навмисно — тест має бути стійким до значень, які система призначає сама, інакше він розсиплеться при першому ж перезапуску на чистій базі.
Коди статусів, які варто знати напамʼять
| Код | Назва | Значення для тесту |
|---|---|---|
| 200 | OK | успіх, тіло очікуване |
| 201 | Created | ресурс створено; часто в тілі — новий обʼєкт, іноді заголовок Location |
| 204 | No Content | успіх без тіла (типово для DELETE); не парсити тіло |
| 400 | Bad Request | некоректний запит (структура/синтаксис) |
| 401 | Unauthorized | немає/невалідна автентифікація |
| 403 | Forbidden | автентифікація є, прав недостатньо |
| 404 | Not Found | ресурсу немає |
| 409 | Conflict | конфлікт стану (напр., дублікат) |
| 422 | Unprocessable Content | синтаксис ок, але бізнес-валідація не пройдена |
| 429 | Too Many Requests | перевищено ліміт запитів |
| 500 | Internal Server Error | збій на сервері |
| 503 | Service Unavailable | сервіс тимчасово недоступний |
Розрізняйте 401 і 403 (не автентифікований проти не авторизований) та 400 і 422 (зламаний запит проти валідного за формою, але неприйнятного за бізнес-правилами) — ці пари постійно фігурують у негативних тест-кейсах.
Пагінація: offset/limit і cursor
Колекції бувають великими, тому API віддає їх порціями (сторінками). Дві поширені моделі.
Offset/limit (зсув і ліміт). Клієнт каже «пропусти N записів, віддай наступні M».
GET /api/v1/tests?offset=40&limit=20
Проста й інтуїтивна модель, дозволяє стрибати на довільну сторінку. Слабке місце — нестабільність при даних, що змінюються: якщо між запитами двох сусідніх сторінок хтось вставив або видалив запис, ви можете пропустити елемент або отримати дубль. Для тестів це означає ризик флаку в паралельних прогонах на спільній базі.
Cursor (курсор). Замість числового зсуву сервер віддає непрозорий маркер, що вказує на позицію в наборі, і клієнт передає його в наступному запиті.
GET /api/v1/tests?limit=20
GET /api/v1/tests?limit=20&cursor=eyJpZCI6MTIzfQ
| offset/limit | cursor | |
|---|---|---|
| Перехід на довільну сторінку | легко | зазвичай ні (лише вперед/назад) |
| Стабільність при змінах даних | слабка | висока |
| Читабельність параметра | зрозуміла | непрозорий токен |
Що з цього корисно для тестів: коли перевіряєте пагінацію, не хардкодьте, що на певній сторінці лежить певний запис — на спільному чи «живому» середовищі дані змінюються. Перевіряйте інваріанти: розмір сторінки не перевищує limit; сусідні сторінки не перетинаються; курсор наступної сторінки присутній, поки є дані, і зникає (або приходить порожня сторінка) в кінці. Такі асерти не залежать від конкретного вмісту й не флакають.
Rate limiting і статус 429
Щоб захиститися від перевантаження, API обмежує кількість запитів на одиницю часу з одного клієнта чи ключа. При перевищенні ліміту сервер відповідає 429 Too Many Requests.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{ "error": "rate limit exceeded" }
Заголовок Retry-After підказує, скільки чекати перед повторною спробою (у секундах або як HTTP-дата). За специфікацією він необовʼязковий, але його часто надсилають. Багато API також віддають лічильники ліміту через заголовки на кшталт X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Це де-факто конвенція, а не частина HTTP-стандарту: їхні точні назви й семантику звіряйте з документацією конкретного API. Існує активна IETF-чернетка (draft-ietf-httpapi-ratelimit-headers), що стандартизує поля RateLimit і RateLimit-Policy без префікса X-, але вона поки не набула статусу RFC.
Для AQA rate limiting — часта прихована причина флаку, особливо в паралельних прогонах, коли десятки тестів гатять в API з одного токена. Практичні прийоми: поважати Retry-After (робити паузу саме на вказаний час, а не фіксовану), додавати експоненційну затримку з повторами (backoff) на транспортному рівні клієнта, розводити навантаження між тестовими акаунтами/ключами. І окремо: сам сценарій rate limiting теж треба тестувати — навмисно перевищити ліміт і переконатися, що приходить саме 429 (а якщо API обіцяє Retry-After — то з коректним значенням), а не 500 чи мовчазне зависання.
GraphQL проти REST (оглядово)
GraphQL — альтернативний підхід до API, який Facebook почав розробляти в 2012 році й відкрив публічно у 2015-му. Головні відмінності від REST:
- Один ендпоінт (зазвичай
POST /graphql) замість багатьох URL-ресурсів. - Клієнт описує, які саме поля йому потрібні, у мові запитів. Сервер повертає рівно їх — ні більше, ні менше. Це вирішує дві проблеми REST: over-fetching (прийшло більше даних, ніж треба) і under-fetching (довелося робити кілька запитів, щоб зібрати картину).
query {
user(id: 42) {
name
posts(first: 5) { title }
}
}
Наслідки для тестування:
- У GraphQL більшість відповідей приходить зі статусом
200, навіть коли є помилки — вони лежать у поліerrorsтіла. Тому перевірка «лише статусу» тут ще небезпечніша, ніж у REST: обовʼязково інспектуйтеdataтаerrorsу тілі. - Форма відповіді визначається запитом, а не сервером, тож ваш асерт «знає» очікувану структуру заздалегідь.
- Кешування й версіонування працюють інакше, ніж у REST: GraphQL зазвичай еволюціонує схему без явних версій — через застарілі (deprecated) поля.
REST не «гірший» за GraphQL — вони розвʼязують різні задачі. Ваша робота як тестувальника — розуміти, з яким стилем маєте справу, бо від цього залежить, де шукати помилки у відповіді.
API-контракт і contract testing
Контракт (contract) — це формальна домовленість про те, як виглядає API: які ендпоінти існують, які приймають параметри й тіла, що і в якому форматі повертають, які статуси можливі. Часто контракт записують машиночитано — для REST це найчастіше OpenAPI (раніше відомий як Swagger), для GraphQL — схема (schema), для тіл — JSON Schema.
Contract testing (контрактне тестування) перевіряє, що реалізація відповідає цьому контракту: чи не змінилася структура відповіді, чи не зникло поле, чи не змінився його тип. Розрізняють два напрями:
- Валідація за схемою — прогнати відповідь через JSON Schema/OpenAPI й переконатися, що вона відповідає опису.
- Consumer-driven contracts — споживач API описує свої очікування, а постачальник тестує, що їх не порушує (класичний інструмент — Pact).
Чому це критично: найпідступніші поломки — не «сервіс упав» (це видно одразу), а «поле created_at тепер приходить як число замість рядка» або «status тепер може бути null». UI чи інший сервіс мовчки ламається на проді. Контрактний тест ловить такі зміни на межі сервісів ще до релізу. Для AQA це часто найдешевша й найстабільніша частина API-покриття: схема не залежить від конкретних значень, тож не флакає й не потребує підготовки складного стану.
Версіонування API
API живе й змінюється, але старі клієнти мають продовжувати працювати. Тому вводять версії. Поширені підходи:
| Підхід | Приклад | Коментар |
|---|---|---|
| У шляху URL | /api/v1/users, /api/v2/users | найпоширеніший, найпомітніший |
| Через заголовок | Accept: application/vnd.example.v2+json | «чистіші» URL, складніше для клієнта |
| Через query-параметр | /api/users?version=2 | простий, але засмічує параметри |
Незалежно від підходу, ключове правило сумісності: у межах однієї версії допустимо додавати поля, але прибирати чи змінювати тип наявних — ламальна зміна (breaking change), яка вимагає нової версії. Звідси прямий висновок для написання асертів: перевіряйте наявність і тип потрібних вам полів, але не падайте від появи нових, невідомих полів у відповіді. Тест, який жорстко звіряє повний обʼєкт «поле в поле», зламається від безпечного розширення API — а мав би пропустити його. Перевіряйте те, що вам справді потрібно (підмножину), а не рівність усього тіла.
API-тестування проти UI-тестування
Це не «або-або», а два рівні піраміди тестування з різними сильними сторонами.
| Критерій | API-тести | UI-тести |
|---|---|---|
| Швидкість | висока (мілісекунди — десятки мс) | низька (секунди на дію) |
| Стабільність | висока, менше флаку | чутливі до верстки, таймінгів, локаторів |
| Що покривають | бізнес-логіку, контракти, дані | реальний шлях користувача, візуал, інтеграцію фронта |
| Підготовка стану | швидка, прямими викликами | повільна, через клацання |
| Чутливість до змін верстки | нечутливі | ламаються від зміни розмітки |
Практичний баланс: бізнес-правила, валідації, права доступу, крайові випадки покривайте на рівні API — там вони швидкі й надійні. UI-тестами перевіряйте те, що можна перевірити лише через інтерфейс: що дані справді відрендерилися, що сценарій користувача проходиться, що фронтенд коректно склеює виклики. І окремо — API часто найкращий інструмент для підготовки й прибирання стану в UI-тестах: створити користувача чи тестові дані одним POST замість проходу майстра з десяти екранів. Це і швидше, і стабільніше, і зменшує кількість місць, де тест може впасти не по суті.
Ідемпотентність і стабільність тестів
Повернемося до ідемпотентності, бо вона напряму годує надійність автотестів.
Ідемпотентна операція дає той самий кінцевий стан незалежно від того, скільки разів її повторили. Це відрізняє її від однакового статус-коду: DELETE /users/42 двічі поспіль залишає систему в тому самому стані (користувача немає), хоча перший виклик може повернути 204, а другий — 404. Кінцевий стан ідемпотентний; проміжна відповідь — ні.
Чому це рятує тести:
- Стійкість до повторів (retry). Мережа моргнула, клієнт зробив повторну спробу — з ідемпотентним
PUT/DELETEце безпечно й не створює дублів. ЗPOSTповтор може створити другий обʼєкт, і саме тут народжуються «привиди» в базі, що потім валять інші тести. - Ідемпотентні фази підготовки/прибирання. Setup, який спершу видаляє сутність, а потім створює, приводить систему до відомого стану незалежно від того, що там лишилося від попереднього прогону. Це основа ізоляції стану між тестами.
- Захист від дублів у самому API. Багато платіжних і критичних API підтримують ключ ідемпотентності — клієнт передає унікальний токен, і сервер гарантує, що повтор із тим самим токеном не виконає операцію двічі.
POST /api/v1/payments HTTP/1.1
Content-Type: application/json
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
{ "amount": 100, "currency": "USD" }
Заголовок Idempotency-Key — це конвенція, популяризована окремими платіжними API, і предмет IETF-чернетки, а не універсальний HTTP-стандарт (на момент написання чернетка не набула статусу RFC). Тому його підтримку й точну назву звіряйте з документацією конкретного API.
Звідси загальний принцип надійності: тест стабільний тоді, коли не залежить від невідомого початкового стану й не залишає по собі сміття. Ідемпотентні операції, самодостатні (stateless) запити, підготовка даних через API замість UI та асерти на інваріанти замість жорстких значень — ось інструментарій, який REST дає для боротьби з флаком.
Що таке REST? Це протокол чи стандарт?
Ні те, ні інше: REST (Representational State Transfer) — це архітектурний стиль, який Рой Філдінг описав у дисертації 2000 року. Він не диктує конкретні заголовки чи формати, а задає набір обмежень: клієнт-сервер, відсутність стану (statelessness), кешованість, однаковий інтерфейс, багатошаровість і необовʼязковий «код на вимогу». Система, що дотримується цих обмежень, вважається RESTful. На практиці термін розмився: «REST API» часто називають будь-який HTTP-API з JSON-відповідями, і більшість із них RESTful лише частково — наприклад, HATEOAS майже ніхто не реалізує. Для тестувальника цінність обмежень практична: якщо API їх дотримується, поведінка передбачувана — однотипні URL, стандартні методи, самодостатні запити, які легко повторювати ізольовано.
Що таке ресурс і ендпоінт? Як виглядає «правильний» REST-URL?
Ресурс — це сутність, якою оперує система (користувач, замовлення, тест-ран), а ендпоінт — конкретна URL-адреса, за якою доступний ресурс чи колекція. У REST мислять іменниками: немає ендпоінта «створити користувача», є ресурс /users, а дію задає HTTP-метод. Конвенції такі: колекція — множина (/api/v1/users), ідентифікатор — окремий сегмент шляху (/api/v1/users/42), вкладений ресурс — продовження шляху (/api/v1/users/42/orders), а фільтри, сортування й пагінація — через query-параметри (/api/v1/orders?status=paid). Дієслова в URL (/getUser, /createOrder) — антипатерн, бо дублюють те, що вже каже метод.
Як HTTP-методи відповідають CRUD-операціям?
Create — це POST на колекцію, Read — GET, Update — PUT (повна заміна ресурсу) або PATCH (часткова зміна), Delete — DELETE. Ключова різниця між PUT і PATCH: PUT замінює ресурс цілком, тож повторний виклик нічого не змінює (ідемпотентний), а PATCH вносить часткову зміну, і його ідемпотентність залежить від семантики самої зміни. Окрім CRUD-четвірки є HEAD (як GET, але тільки заголовки, без тіла) та OPTIONS (запит про можливості ресурсу, використовується в CORS-preflight). Точні визначення поведінки методів закріплені в RFC 9110 «HTTP Semantics» — при спірних крайових випадках у контрактних тестах звірятися варто саме з нею.
Які типи даних існують у JSON?
Шість: чотири прості — рядок, число, булеве значення, null — і два структурні — обʼєкт і масив. Важливі нюанси, на яких ловлять на співбесідах: числа не поділяються на цілі й дробові, є один тип number; типу «дата» немає — дати передають рядком (зазвичай ISO 8601, як-от "2026-07-08T10:15:30Z") або числом Unix-часу, і формат — частина контракту; ключі обʼєктів — завжди рядки в подвійних лапках, одинарні недопустимі; коментарів немає — коментар робить документ невалідним. І окремо: null — це значення, а не відсутність поля. {"phone": null} і порожній обʼєкт без цього поля — різні контракти, і в тестах їх треба розрізняти явно.
Чим обʼєкт відрізняється від масиву в JSON і чому це важливо для асертів?
Обʼєкт — невпорядкований набір пар «ключ-значення» у фігурних дужках, масив — впорядкований список у квадратних. Практичне правило: звертаєшся за назвою — обʼєкт (user.email), за позицією — масив (users[0]). Реальні відповіді комбінують обидва й бувають глибоко вкладеними: шлях на кшталт profile.contacts[0].value — щоденна рутина при написанні асертів і роботі з JSONPath. Для стабільності тестів критична асиметрія: порядок ключів обʼєкта не гарантований, а порядок елементів масиву — значущий. Тому асерт на послідовність ключів — помилка завжди, а якщо API повертає масив без явного сортування, порядок може «плавати» між запитами й давати флак — сортуйте колекцію перед порівнянням або перевіряйте наявність елементів без привʼязки до позиції.
Що таке серіалізація й десеріалізація?
Серіалізація — перетворення обʼєкта в памʼяті на текст для передавання мережею; десеріалізація — зворотний процес. У JavaScript це JSON.stringify і JSON.parse. Для AQA це не абстракція: майже кожен API-тест десеріалізує тіло відповіді, щоб дістатися до полів, і саме тут ховаються типові падіння. JSON.parse кидає виняток на невалідному JSON — а порожнє тіло, HTML-сторінка помилки чи обірваний потік саме такими і є. Тому надійний тест спершу перевіряє статус і Content-Type, і лише потім парсить тіло — інакше падіння на парсингу замаскує справжню причину. Ще нюанс при підготовці тіла запиту: JSON.stringify перетворює Date на рядок ISO 8601, а поля зі значенням undefined просто зникають.
Чим 401 відрізняється від 403, а 400 від 422?
Це дві пари, які постійно фігурують у негативних тест-кейсах. 401 Unauthorized — автентифікації немає або вона невалідна (токен відсутній чи протух); 403 Forbidden — сервер знає, хто ви, але прав на цю дію недостатньо. Тобто 401 — «не автентифікований», 403 — «не авторизований». Друга пара: 400 Bad Request — запит зламаний на рівні структури чи синтаксису; 422 Unprocessable Content — запит синтаксично коректний, але не проходить бізнес-валідацію (наприклад, email у валідному JSON, але вже зайнятий). У тестах прав доступу важливо асертити саме очікуваний код із пари, а не «будь-яку 4xx» — інакше тест не відрізнить зламану автентифікацію від зламаної авторизації.
Чому в API-тесті треба перевіряти і статус-код, і тіло відповіді?
Бо вони відповідають на різні запитання: статус каже, як сервер поставився до запиту на рівні протоколу, а тіло — що саме повернулося. Перевірка лише статусу — джерело хибно-зелених тестів: сервер може повернути 200 OK з тілом {"error": "not found"} (антипатерн, який трапляється частіше, ніж хотілось би), або 201 Created, де поле id — null, і наступний крок сценарію не матиме з чим працювати. Мінімальний надійний асерт перевіряє три речі: статус, Content-Type і структуру/значення тіла. Для згенерованих системою значень фіксують тип і наявність поля, а не конкретне значення — у Playwright це expect.any(Number) для id, — інакше тест розсиплеться при першому прогоні на чистій базі.
Що таке безпечний та ідемпотентний HTTP-метод? Чому POST не ідемпотентний?
Безпечний (safe) метод не змінює стан на сервері — GET лише читає, тож його можна повторювати без побічних ефектів. Ідемпотентний (idempotent) метод при повторному виклику з тими самими даними дає той самий кінцевий стан: PUT замінює ресурс цілком, тож повтор нічого не додає; DELETE /users/42 двічі поспіль залишає систему в тому ж стані — користувача немає. POST не ідемпотентний, бо два POST /users створять двох користувачів. Тонкий момент, який відрізняє сильну відповідь: ідемпотентність — про кінцевий стан, а не про однаковий статус-код. Перший DELETE може повернути 204, другий — 404, але стан системи ідентичний, тож метод ідемпотентний.
Чим JSON відрізняється від XML і де ви досі зустрінете XML?
JSON компактніший, має вбудовані типи (number, boolean, null, обʼєкти, масиви) і не має ані коментарів, ані атрибутів, ані просторів імен. XML громіздкіший через закривні теги, все в ньому — текст (типи задає схема XSD/DTD), зате є атрибути, коментарі й namespaces. Для валідації JSON використовують JSON Schema, для пошуку по документу — JSONPath; у XML це XSD і XPath відповідно. XML досі живий у legacy-системах, SOAP-сервісах і деяких банківських та урядових інтеграціях. Практична різниця для тестувальника: якщо тестуєте XML-API, шлях до значення описується через XPath — логіка та сама, що й з локаторами в DOM.
Чим application/x-www-form-urlencoded відрізняється від JSON-тіла?
Form-urlencoded — плоскі пари ключ=значення, зʼєднані &, зі спецсимволами в percent-encoding (пробіл кодується як +); це формат, який історично надсилає HTML-форма без явного enctype, і його досі використовують форми логіну та OAuth token-запити. JSON-тіло здатне передати вкладені обʼєкти, масиви й типи даних, тому домінує в сучасних REST API. Формат тіла оголошується заголовком Content-Type, і він має відповідати реальному вмісту. Класична причина, чому «в браузері працює, а в тесті ні»: тест шле JSON, а сервер очікує form-urlencoded (або навпаки) і повертає 400 чи 415 Unsupported Media Type. Рецепт: підглянути справжній запит у DevTools на вкладці Network і скопіювати Content-Type та формат тіла точно.
Навіщо API-тести, якщо є UI-тести? Як їх балансувати?
Це два рівні піраміди тестування з різними сильними сторонами, а не «або-або». API-тести швидкі (мілісекунди проти секунд на дію), стабільніші (нечутливі до верстки, локаторів і таймінгів) і дають прямий контроль над станом системи. UI-тести покривають те, що інакше не перевірити: реальний шлях користувача, рендеринг даних, коректну склейку викликів фронтендом. Практичний баланс: бізнес-правила, валідації, права доступу й крайові випадки — на рівні API; UI — лише те, що видно тільки через інтерфейс. І окремий прийом, що ілюструє зрілість кандидата: API — найкращий інструмент підготовки й прибирання стану для UI-тестів — створити користувача одним POST замість проходу майстра з десяти екранів швидше, стабільніше й зменшує кількість місць, де тест впаде не по суті.
Які моделі пагінації ви знаєте і як тестувати пагінацію без флаку?
Дві поширені моделі: offset/limit («пропусти N, віддай M» — просто, дозволяє стрибати на довільну сторінку) і cursor (сервер віддає непрозорий маркер позиції, клієнт передає його в наступному запиті). Слабкість offset/limit — нестабільність на живих даних: якщо між запитами сусідніх сторінок хтось вставив або видалив запис, ви пропустите елемент або отримаєте дубль; cursor цю проблему знімає, але зазвичай не дає довільного стрибка по сторінках. Для тестів звідси головне правило: не хардкодити, що на певній сторінці лежить певний запис — на спільному середовищі дані змінюються. Замість цього перевіряють інваріанти: розмір сторінки не перевищує limit, сусідні сторінки не перетинаються, курсор наступної сторінки є, поки є дані, і зникає в кінці. Такі асерти не залежать від конкретного вмісту й не флакають у паралельних прогонах.
Що таке rate limiting і як автотести мають поводитися з 429?
Rate limiting — обмеження кількості запитів на одиницю часу з одного клієнта чи ключа; при перевищенні сервер відповідає 429 Too Many Requests, часто з заголовком Retry-After, який підказує, скільки чекати перед повтором. Для AQA це часта прихована причина флаку в паралельних прогонах, коли десятки тестів гатять в API з одного токена. Практичні прийоми: поважати Retry-After (пауза саме на вказаний час, а не фіксована), експоненційний backoff з повторами на транспортному рівні клієнта, розведення навантаження між тестовими акаунтами чи ключами. Лічильники на кшталт X-RateLimit-Remaining — де-факто конвенція, а не HTTP-стандарт, тож їхні назви й семантику звіряють з документацією конкретного API. І окремо: сам rate limiting — теж обʼєкт тестування: навмисно перевищити ліміт і переконатися, що приходить саме 429 з коректним Retry-After, а не 500 чи мовчазне зависання.
Чим GraphQL відрізняється від REST і що це змінює в тестуванні?
GraphQL має один ендпоінт (зазвичай POST /graphql) замість багатьох URL-ресурсів, і клієнт сам описує в запиті, які поля йому потрібні, — сервер повертає рівно їх. Це вирішує дві проблеми REST: over-fetching (прийшло більше даних, ніж треба) і under-fetching (кілька запитів, щоб зібрати картину). Для тестування головна пастка: у GraphQL більшість відповідей приходить зі статусом 200 навіть при помилках — вони лежать у полі errors тіла, тож перевірка «лише статусу» тут ще небезпечніша, ніж у REST: обовʼязково інспектувати і data, і errors. Зате форма відповіді визначається самим запитом, тож асерт «знає» очікувану структуру заздалегідь. Версіонування теж інше: схема зазвичай еволюціонує без явних версій, через deprecated-поля.
Що таке API-контракт і contract testing? Навіщо це, якщо є звичайні API-тести?
Контракт — формальна домовленість про те, як виглядає API: ендпоінти, параметри, формати тіл, можливі статуси; машиночитано його записують в OpenAPI (колишній Swagger) для REST, у схемі для GraphQL, у JSON Schema для тіл. Contract testing перевіряє, що реалізація відповідає контракту, і має два напрями: валідація відповіді за схемою та consumer-driven contracts, де споживач описує свої очікування, а постачальник тестує, що їх не порушує (класичний інструмент — Pact). Цінність у тому, що найпідступніші поломки — не «сервіс упав», а «поле created_at тепер число замість рядка» чи «status може бути null»: UI або сусідній сервіс мовчки ламається на проді. Контрактний тест ловить це на межі сервісів ще до релізу. Для AQA це часто найдешевша частина API-покриття: схема не залежить від конкретних значень, тож не флакає й не потребує підготовки складного стану.
Як версіонують API і що таке breaking change? Як писати асерти, стійкі до еволюції API?
Три поширені підходи: версія в шляху URL (/api/v1/users — найпоширеніший), через заголовок (Accept: application/vnd.example.v2+json — «чистіші» URL, але складніше для клієнта) і через query-параметр. Ключове правило сумісності: у межах однієї версії можна додавати поля, але прибирати чи змінювати тип наявних — ламальна зміна (breaking change), яка вимагає нової версії. Звідси прямий висновок для автотестів: перевіряйте наявність і тип потрібних вам полів, але не падайте від появи нових, невідомих. Тест, який жорстко звіряє повний обʼєкт «поле в поле», зламається від безпечного розширення API — а мав би його пропустити. Тому асертимо підмножину (наприклад, toMatchObject замість строгої рівності всього тіла) — те, що справді потрібно сценарію.
Як ідемпотентність повʼязана зі стабільністю автотестів?
Ідемпотентність напряму годує надійність тестів у трьох місцях. Перше — стійкість до повторів: мережа моргнула, клієнт зробив retry — з ідемпотентними PUT/DELETE це безпечно, а повторений POST може створити дубль, і саме такі «привиди» в базі потім валять інші тести. Друге — ідемпотентні фази підготовки й прибирання: setup, який спершу видаляє сутність, а потім створює, приводить систему до відомого стану незалежно від того, що лишилося від попереднього прогону, — це основа ізоляції між тестами. Третє — захист від дублів у самому API: платіжні й критичні API підтримують ключ ідемпотентності (заголовок на кшталт Idempotency-Key з унікальним токеном), і сервер гарантує, що повтор з тим самим ключем не виконає операцію двічі; це конвенція окремих API і предмет IETF-чернетки, а не HTTP-стандарт, тож підтримку звіряють з документацією. Загальний принцип: тест стабільний, коли не залежить від невідомого початкового стану й не залишає по собі сміття — ідемпотентні операції, stateless-запити, підготовка даних через API та асерти на інваріанти і є цим інструментарієм.
Три кейси, де REST-грамотність вирішує, зелений тест чи хибний результат: надійний асерт, що перевіряє статус, Content-Type і структуру тіла (і не падає від безпечного розширення API), діагностика «в браузері працює, а в тесті 415» через звірку Content-Type у DevTools, і пагінація на інваріантах замість хардкоду вмісту сторінки. Скрізь — що дивитися і чому.
Кейс 1. Мінімальний надійний асерт: статус + Content-Type + підмножина тіла
Спокуса перевірити лише статус-код («прийшло 201 — значить ок») народжує хибно-зелені тести. Сервер може віддати 200 OK з тілом {"error": "not found"}, або 201 Created з полем id: null, з яким наступний крок тесту не має що робити. Тому надійний асерт відповідає на три різні питання: як сервер поставився до запиту (статус), у якому форматі відповів (Content-Type) і що саме повернув (структура й значення тіла).
import { test, expect } from '@playwright/test';
test('POST /users створює користувача й повертає повний обʼєкт', async ({ request }) => {
const res = await request.post('/api/v1/users', {
data: { name: 'Alice', email: 'alice@example.com' },
});
// 1) як сервер поставився до запиту
expect(res.status()).toBe(201);
// 2) у якому форматі відповів — інакше res.json() впаде на HTML-сторінці помилки
expect(res.headers()['content-type']).toContain('application/json');
// 3) що саме повернув — тільки потрібні поля, не рівність усього тіла
const body = await res.json();
expect(body).toMatchObject({
id: expect.any(Number),
name: 'Alice',
email: 'alice@example.com',
});
});
Що дивитися і чому:
expect.any(Number)замість конкретного id. Фіксуємо тип і наявність поля, не привʼязуючись до згенерованого значення. Інакше тест розсиплеться при першому ж перезапуску на чистій базі, де id буде інший.toMatchObject, а неtoEqual. У межах однієї версії API безпечно додавати поля. Асерт, що жорстко звіряє все тіло «поле в поле», зламається від нешкідливого розширення відповіді — а мав би його пропустити. Перевіряйте підмножину, яка вам справді потрібна.- Порядок перевірок не випадковий. Спершу статус і
Content-Type, і лише потімres.json(). Порожнє тіло, HTML-сторінка помилки чи обірваний потік кинуть виняток на парсингу — і падіння наjson()замаскує справжню причину (насправді прийшло 500, а не той обʼєкт).
Кейс 2. «В браузері працює, а в тесті 415»: звірка Content-Type у DevTools
Класична розбіжність: ручний сценарій у браузері проходить, а автотест отримує 400 Bad Request або 415 Unsupported Media Type. Найчастіша причина — тест надсилає тіло в одному форматі, а сервер очікує інший. Форму логіну історично шлють як application/x-www-form-urlencoded (пари ключ=значення, зʼєднані &), а тест за звичкою відправляє JSON — або навпаки.
Перш ніж воювати з тестом, підгляньте справжній запит у DevTools → вкладка Network → проблемний запит → Headers (Content-Type) і Payload (реальне тіло). У браузері форма логіну виглядає так:
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=secret%21
А тест помилково шле JSON, і сервер відповідає 415:
// ❌ сервер логіну очікує form-urlencoded, а тест дає JSON → 415
await request.post('/login', {
data: { username: 'alice', password: 'secret!' }, // Playwright виставить application/json
});
// ✅ формат тіла збігається з тим, що показав DevTools
await request.post('/login', {
form: { username: 'alice', password: 'secret!' }, // application/x-www-form-urlencoded
});
Що дивитися і чому:
Content-Typeмає відповідати реальному тілу. Заголовок оголошує формат; якщо він бреше про вміст, сервер відхилить запит. У Playwrightdata:з обʼєктом даєapplication/json, аform:—application/x-www-form-urlencoded. Скопіюйте формат із DevTools точно, а не за здогадом.- Розрізняйте 400 і 415.
415— сервер не вміє читати оголошений медіатип узагалі;400— тип він приймає, але тіло синтаксично зламане. Це різні діагнози й різні виправлення. - Пробіл у form-urlencoded — це
+, спецсимволи в percent-encoding (secret!→secret%21). Якщо збираєте тіло руками, а не черезform:, легко забути кодування й зловити400.
Кейс 3. Пагінація без хардкоду: перевіряйте інваріанти, а не вміст сторінки
Тест, який стверджує «на другій сторінці лежить користувач Bob», флакне на спільному чи «живому» середовищі: між запитами двох сусідніх сторінок хтось вставив або видалив запис, і в моделі offset/limit ви пропустите елемент або отримаєте дубль. Замість вмісту перевіряйте властивості, які тримаються незалежно від даних: розмір сторінки не перевищує limit, сусідні сторінки не перетинаються, наступна сторінка є, поки є дані.
import { test, expect } from '@playwright/test';
test('пагінація тримає інваріанти незалежно від вмісту', async ({ request }) => {
const limit = 20;
const p1 = await (await request.get(`/api/v1/tests?offset=0&limit=${limit}`)).json();
const p2 = await (await request.get(`/api/v1/tests?offset=${limit}&limit=${limit}`)).json();
// розмір сторінки не перевищує limit
expect(p1.items.length).toBeLessThanOrEqual(limit);
// сусідні сторінки не перетинаються (звіряємо id, не порядок)
const ids1 = new Set(p1.items.map((t) => t.id));
const overlap = p2.items.filter((t) => ids1.has(t.id));
expect(overlap, 'сторінки 1 і 2 перетинаються').toHaveLength(0);
});
Що дивитися і чому:
- Порядок елементів масиву значущий, але без явного сортування «плаває». Якщо API не гарантує сортування, не асертьте позицію (
items[0].id === 42) — звіряйте наявність черезSet, інакше отримаєте флак від запиту до запиту. - На cursor-пагінації інваріанти інші, логіка та сама. Замість стрибка по offset перевіряйте: курсор наступної сторінки присутній, поки є дані, і зникає (або приходить порожня сторінка) в кінці. Непрозорий токен не інспектуйте — покладайтеся на його наявність/відсутність.
- Пагінаційний цикл — прихована причина 429. Багато послідовних запитів з одного токена в паралельному прогоні впираються в rate limiting. Надійний клієнт поважає
Retry-After(пауза саме на вказаний час) і робить backoff, а не фіксовану затримку. Окремо варто протестувати й сам ліміт: навмисно перевищити його й переконатися, що приходить саме429, а не 500 чи мовчазне зависання.
REST і ресурси
- Можу пояснити, що REST — це архітектурний стиль (набір обмежень), а не протокол чи стандарт, і що більшість «REST API» RESTful лише частково.
- Знаю обмеження REST: клієнт-сервер, statelessness, кешованість, однаковий інтерфейс, багатошаровість, code on demand (необовʼязкове).
- Розумію, чому statelessness спрощує тести: кожен запит самодостатній, його легко повторити чи запустити ізольовано.
- Можу пояснити різницю між ресурсом (іменник-сутність) і ендпоінтом (конкретний URL), і чому дієслова в URL (
/getUser) — антипатерн: дію задає HTTP-метод. - Знаю конвенції REST-URL: колекції — множина, id окремим сегментом, а фільтр/сортування/пагінація — через query-параметри після
?.
HTTP-методи, safe та idempotent
- Знаю відповідність CRUD і методів:
GET/POST/PUT/PATCH/DELETE, а такожHEADіOPTIONS(CORS-preflight). - Можу пояснити safe (не змінює стан —
GET) і idempotent (повтор дає той самий кінцевий стан —PUT,DELETE;POST— ні;PATCH— не обовʼязково). - Розумію, чому
DELETEдвічі поспіль ідемпотентний за станом, хоча статус-код другого виклику може бути іншим (204→404).
JSON та структури даних
- Знаю шість типів JSON: рядок, число, булеве,
null, обʼєкт, масив — і що число одне (немає окремихint/float). - Можу пояснити пастки JSON: немає типу «дата» (передають рядком ISO 8601 чи Unix-часом), ключі лише в подвійних лапках, немає коментарів,
null≠ відсутність поля (\{"phone": null\}vs\{\}). - Розумію різницю обʼєкт (звертання за назвою) vs масив (за позицією) і вмію читати шлях у вкладеній структурі (
profile.contacts[0].value). - Розумію, чому порядок ключів обʼєкта не гарантований, а порядок елементів масиву значущий — і як несортований масив дає флак.
- Можу пояснити серіалізацію/десеріалізацію (
JSON.stringify/JSON.parse) і чому парсити тіло треба лише після перевіркиContent-Typeта статусу.
Формати обміну
- Можу порівняти JSON і XML: типи, атрибути, простори імен, коментарі, JSONPath vs XPath, і де XML досі живе (SOAP, legacy).
- Можу пояснити різницю
application/x-www-form-urlencoded(плоскі пари, пробіл —+) vsapplication/json, і що невідповідністьContent-Typeтілу дає 400/415.
Перевірки у тестах
- Розумію, чому перевіряти лише статус недостатньо: мінімальний надійний асерт — статус +
Content-Type+ структура/значення тіла. - Знаю ключові статус-коди напамʼять і різницю пар 401 vs 403, 400 vs 422, а також 201/204/409/429/500/503.
- Можу пояснити, чому асерт має фіксувати тип і наявність поля (
expect.any(Number)), а не хардкодити згенеровані значення.
Масштаб, версії та стабільність
- Можу пояснити offset/limit vs cursor-пагінацію і перевіряти інваріанти (розмір ≤
limit, сторінки не перетинаються), а не конкретний вміст сторінки. - Розумію rate limiting і
429: поважатиRetry-After, робити backoff, розводити токени; знаю, щоX-RateLimit-*— конвенція, не стандарт. - Можу оглядово порівняти GraphQL і REST: один ендпоінт, клієнт описує поля, помилки в тілі при статусі
200. - Можу пояснити API-контракт і contract testing (OpenAPI/JSON Schema, consumer-driven/Pact) і чому це ловить зміну типу поля до релізу.
- Знаю підходи версіонування (шлях/заголовок/query) і правило сумісності: додавати поля можна, прибирати чи міняти тип — breaking change; асерт перевіряє підмножину, не рівність усього тіла.
- Розумію, коли API-тести сильніші за UI (швидкість, стабільність, підготовка стану) і як ідемпотентність та stateless-запити рятують від флаку.
Що таке REST?
Питання
REST — це протокол чи стандарт?