1. Negative-path як частина контракту
Якщо чесно, happy-path — це найпопулярніший спосіб мислення початківця-розробника. Приблизно так: «Я відправив запит, отримав дані, усе гарно — значить, API працює». Проблема в тому, що в реальному житті користувачі, мережа й зовнішні сервіси дуже рідко поводяться так само дисципліновано, як ваш демонстраційний запит у Postman.
Negative-path — це усвідомлена перевірка меж контракту. Ми навмисно робимо запит «не так»: передаємо порожній параметр, неіснуючий ідентифікатор, дивний ліміт, неправильний формат. І дивимося, що робить сервіс. Не для того, щоб «зловити його на помилці», а щоб зрозуміти: як сервіс повідомляє про проблему і який формат відповіді можна вважати частиною договору.
Є одна важлива психологічна пастка, від якої хочеться одразу позбутися. У звичайному мовленні слово «помилка» означає «усе погано». У контрактному мисленні «помилка» часто означає «очікувана гілка». Наприклад, запит «деталі книги за неіснуючим ID» майже зобов’язаний повернути 404 Not Found або щось еквівалентне за змістом. Це не «зламалося», а «сервіс чесно сказав: такого ресурсу немає».
Чому це важливо для нашого проєкту ReadLater Starter саме зараз, ще на етапі Postman? Тому що згодом, не сьогодні, ви будете писати код клієнта, а код має розуміти щонайменше три різні наслідки:
- усе гаразд і дані є;
- усе гаразд, але даних немає (порожній результат);
- сервіс повідомляє, що запит некоректний, ресурсу немає або стався конфлікт, тобто статус і/або тіло відповіді вказують на проблему.
Якщо ви заздалегідь не побачили ці гілки через Postman, то в коді будете навмання вгадувати, що означає «порожня відповідь» і чому іноді прилітає 404, а іноді 200 із порожнім списком.
2. Negative-path і мережеві збої
Одна з найчастіших пасток початківця — вважати negative-path «будь-яким невдалим результатом». У підсумку людина вимикає Wi‑Fi, бачить у Postman Could not get any response і радісно каже: «Ось він, негативний сценарій!». Це корисний життєвий досвід, але це не перевірка контракту, а перевірка того, що ви вмієте страждати.
Negative-path — це те, що можна відтворити передбачувано і пояснити двома фразами: «Я відправив такий-то запит із таким-то входом, сервіс повернув такий-то статус і таку-то форму відповіді». Мережевий збій — це окрема категорія транспортних проблем, а ми зараз говоримо про межі HTTP/JSON-договору.
Щоб не плутатися, тримайте в голові просту схему. Вона не академічна, а практична: допомагає швидко зрозуміти, що саме ви спостерігаєте в Postman.
flowchart TD
A["Відправили запит"] --> B{"Отримали HTTP-відповідь?"}
B -- ні --> N["Мережевий збій: timeout / DNS / немає зʼєднання"]
B -- так --> C{"Status 2xx?"}
C -- так --> D{"Дані \"за змістом\" є?"}
D -- так --> HP["Happy-path: успіх + корисні дані"]
D -- ні --> EP["Граничний сценарій: успіх + порожній результат"]
C -- ні --> ER["Negative-path: 4xx/5xx як частина контракту"]
Зверніть увагу на гілку «успіх + порожній результат». Вона часто ламає мозок. За HTTP усе чудово (200 OK), а за змістом — нічого не знайдено. І це нормально. Це не обов’язково помилка; інколи це цілком коректна контрактна відповідь. Особливо для пошуку.
Щоб зробити різницю зовсім «в лоб», ось невелика таблиця, яку зручно тримати поруч, поки ви навчаєтесь:
| Що сталося | Як виглядає в Postman | Це про контракт? | Що ми робимо зараз |
|---|---|---|---|
| Сервіс не відповів | Could not get any response, timeout, помилка з’єднання | Скоріше ні | Не змішуємо з negative-path, фіксуємо як окремий операційний факт |
| Сервіс відповів 4xx/5xx | Є код статусу і заголовки, часто є body | Так | Вивчаємо статус і форму відповіді як частину договору |
| Сервіс відповів 2xx, але даних немає | 200 OK, body порожній за змістом (наприклад, items: []) | Так | Фіксуємо поведінку як «граничний сценарій», не називаємо це «зламалося» |
3. Відтворювані negative-path сценарії
Negative-path легко перетворити на хаос: можна нагенерувати сто запитів, половина з яких «інколи працює». Нам так не треба. Нам потрібні сценарії, які ви зможете запустити через тиждень і отримати приблизно таку саму поведінку, без шаманства й ворожіння на кавовій гущі. А каву, як відомо, краще пити, ніж ворожити на ній.
Головні критерії хорошого negative-path сценарію для зовнішнього API прості. Він має бути відтворюваним, безпечним для провайдера, без DDoS і без «ліміту 1 000 000», а ще — перевіряти конкретну межу: «що буде, коли запит порожній», «що буде, коли ресурсу немає», «що буде, коли параметр невалідний».
Нижче ми розберемо negative-path для двох базових сценаріїв каталогу: пошук і деталі. Ми спеціально говоритимемо не про те, «як правильно за REST», а про те, «як реально побачити поведінку зовнішнього API й зафіксувати її».
Negative-path для search: порожнеча й межі
Пошук — ідеальне місце для негативних сценаріїв, тому що він природно допускає ситуацію «нічого не знайдено». Саме тому важливо розділяти «помилку запиту» і «порожній результат». Якщо ви робите q=asdkjasdkjasd, сервіс може чесно відповісти 200 OK і порожнім списком — це не помилка, а відсутність збігів.
Ось кілька сценаріїв, які зазвичай добре працюють і не залежать від того, які саме книги є у провайдера. Формально це мінінабір, але вже він дає вам майже все потрібне розуміння.
| Сценарій | Приклад запиту | Що ми очікуємо побачити |
|---|---|---|
| Порожній запит | {{baseUrl}}/search?q=&limit={{limit}} | Часто 400, інколи 200 із порожніми даними — залежить від провайдера |
| Запит із пробілів | q= | Часто трактується як порожній |
| «Сміттєвий рядок» | q=asdkjasdkjasd | Зазвичай 200 + порожній список/count=0 |
| Граничний ліміт | limit=0 або |
Часто 400, інколи значення «спадає» до типового ліміту |
| Надто великий ліміт | limit=9999 | Або 400, або сервіс сам уріже ліміт |
Тут важлива думка: ми не намагаємося заздалегідь вгадати правильну відповідь, ми намагаємося з’ясувати контракт. Якщо сервіс на порожній q= повертає 200 OK і порожній список — це дивно, але це факт. Для вас, як для майбутнього автора клієнта, це означає: «порожній q не призводить до 400, отже клієнт має валідувати це самостійно, інакше буде беззмістовний запит».
Negative-path для details: неіснуючий ID і неправильний формат
З деталями зазвичай простіше: у нас є один ресурс — книга — і її ідентифікатор. Negative-path тут часто виглядає як «цього ресурсу немає» або «ідентифікатор невалідний». Головне — вибрати ID так, щоб він точно не існував. Не «мені здається, що не існує», а «я вибрав завідомий маркер».
Найпрактичніший підхід — завести окрему змінну середовища, наприклад missingBookId, і покласти туди очевидний маркер:
# Змінна середовища для сценарію "книги точно немає"
missingBookId = DOES_NOT_EXIST
Тоді ваш negative-запит виглядає так:
{{baseUrl}}/books/{{missingBookId}}
Чому так краще, ніж «вручну в URL»? Тому що змінна відразу робить сенс сценарію очевидним. Коли через тиждень ви відкриєте колекцію, вам не доведеться згадувати, чому там був ID BK-000000. За назвою змінної все зрозуміло: це ID, якого немає.
Є ще один цікавий тип negative-path для details: невалідний формат. Наприклад, якщо провайдер очікує буквено-цифровий ID, можна перевірити щось на кшталт !!! або 123%. Деякі сервіси відповідають 400 Bad Request, деякі — 404 Not Found (бо їм простіше сказати «немає такого ресурсу», ніж пояснювати формат). І це теж частина контракту: інколи сервіс не розкриває, чи валідний формат, щоб не давати зайвої інформації.
І ще один важливий нюанс саме для нашої зв’язки «пошук → деталі». Якщо в середовищі є bookId, ви легко можете випадково обманути самі себе: негативний пошук нічого не знайшов, але bookId залишився від попереднього успішного пошуку, і деталі далі «ніби працюють». Це виглядає так, ніби chained-сценарій стабільний, хоча насправді він просто використовує старе значення.
Якщо ви вже використовуєте Tests у search-запиті, то найпростіший прийом — явно очищати bookId перед тим, як намагатися зберегти новий. Тоді «порожній пошук» не залишить у середовищі старий ID.
// Важливо: не даємо старому bookId "протекти" в наступний запуск сценарію
pm.environment.unset("bookId"); // прибираємо старе значення
const body = pm.response.json(); // читаємо JSON-відповідь
// Якщо щось знайшлося — зберігаємо id першої книги для chained-сценарію
if (body.items && body.items.length > 0) {
pm.environment.set("bookId", body.items[0].id);
}
Цей шматок коду маленький, але він робить chained-сценарії чеснішими: якщо пошук нічого не повернув, ви це побачите не тільки очима, а й за тим, що bookId більше не існує.
4. Колекція без test2/test3
Коли колекція росте, вона легко перетворюється на музей випадкових експериментів: new request, new request (2), test, bad, bad2-final. Це нормально для перших 10 хвилин, але жахливо як робочий артефакт курсу. Negative-path особливо любить розмножуватися, тому тут дисципліна потрібна одразу.
Хороша колекція — це коли за назвами папок і запитів можна відновити сценарій, навіть не відкриваючи URL. Ми не намагаємося зробити ідеальний каталог тестів, ми просто хочемо, щоб ваша колекція була читабельною, як зміст: що перевіряємо і навіщо.
Зручний компромісний формат для нашого каталогу може виглядати так:
Каталог/
├─ Пошук/
│ ├─ happy-path / за запитом
│ ├─ negative / порожній-запит
│ ├─ negative / сміттєвий-запит
│ └─ edge / limit-zero
└─ Деталі/
├─ happy-path / за-id (використовує {{bookId}})
├─ negative / відсутній-id (використовує {{missingBookId}})
└─ edge / невалідний-формат-id
Зверніть увагу на слово edge. Це не обов’язково, але часто допомагає відокремити явно поганий запит (negative) від граничного значення (edge). limit=0 — це не завжди помилка користувача, це інколи дивне, але можливе значення. У підсумку ви швидше розумієте, що саме перевіряли.
Ще одна практична порада: не використовуйте в negative-запитах змінні, які потрібні для happy-потоку, якщо це може ламати сценарій. Наприклад, {{bookId}} — це змінна, яку ми витягуємо з успішного пошуку. Для negative details значно безпечніше використовувати окрему {{missingBookId}}, щоб ви випадково не перезаписали «робочий» ID і не зламали собі ж smoke-набір.
5. Що порівнювати у відповідях negative-path
Є спокуса дивитися лише на body, бо «там же JSON». Але в HTTP контракт — це три речі одночасно: status, headers, body. І в negative-path це особливо важливо, бо body інколи взагалі може бути не JSON. Так, деякі сервіси люблять повертати HTML-сторінку помилки, і це теж реальність, навіть якщо нам це не подобається.
Коли ви фіксуєте negative-path, вам потрібно навчитися ставити собі однакові запитання. Не «що за дурниця прилетіла», а «як сервіс повідомляє про проблему». Для цього зручно тримати маленьку таблицю: що дивитися залежно від сценарію.
| Тип negative/edge сценарію | На що дивимося у відповіді | Чому це важливо |
|---|---|---|
| Некоректний запит (наприклад, порожній q) | status (часто 400), Content-Type, чи є body і яка його форма | У коді клієнта ви маєте розрізняти «погане введення» і «нічого не знайдено» |
| Відсутній ресурс (неіснуючий ID) | status (часто 404), чи є корисна помилка в body | Клієнт має вміти сказати «книги немає», а не «усе зламалося» |
| Порожній результат пошуку | 200, форма body (items/count), порожній список проти відсутнього поля | Це нормальна гілка роботи пошуку, а не помилка транспорту |
| Граничні параметри (limit=0/-1/9999) | status (400 або 200), чи не відбувається «тихе виправлення» параметра | Ви дізнаєтесь, наскільки сервіс суворий і чи потрібна валідація на боці клієнта |
| Внутрішня помилка сервісу | status (500/503), може бути текстова помилка | Це вже ближче до операційного світу, але статус усе одно частина контракту |
Давайте окремо проговоримо «порожній результат», бо він найчастіше викликає непорозуміння. Для пошуку «нічого не знайдено» — це цілком коректна відповідь, і найчастіше її роблять через 200 OK і порожній список. Приблизний shape може бути таким:
{
"items": [],
"count": 0
}
Це не означає, що запит поганий. Це означає, що запит коректний, просто збігів немає. І це важливо відрізняти від сценарію «запит невалідний», де частіше очікується 400 Bad Request.
Для details, навпаки, «нічого не знайдено» зазвичай означає, що ресурсу не існує, і тоді логічніше побачити 404 Not Found. Але зовнішні сервіси інколи обирають інші підходи. Ваше завдання на цьому етапі — не сперечатися з реальністю, а зафіксувати, яка реальність у провайдера.
6. Мінімальні перевірки в Postman Tests
Зараз важливо не скотитися в окремий курс про тестування API через Postman. Нам потрібен дуже легкий шар автоматичної перевірки, щоб negative-path був не «я колись бачив, що там було 404», а «я запускаю — і Postman підтверджує, що контракт не змінився». Це особливо корисно під час самостійного навчання: сьогодні ви подивилися, завтра забули, післязавтра провайдер змінився — і ви не розумієте, ви помилилися чи змінився світ.
У negative-path логіка проста: ми очікуємо певний статус і саме його перевіряємо. Не «щоб тест був зеленим», а щоб зафіксувати домовленість. Наприклад, запит із порожнім q (якщо ваш провайдер відповідає 400) можна оформити так:
pm.test("Порожній запит повертає 400", () => {
// Фіксуємо контракт: саме для цього сценарію очікуємо 400
pm.response.to.have.status(400);
});
Якщо ваш провайдер замість 400 повертає 200 і порожній список, тоді перевірка буде іншою, і це нормально:
// Дістаємо тіло відповіді, щоб перевірити форму результату
const body = pm.response.json();
pm.test("Пошук повертає порожні items", () => {
// Важливо: перевіряємо структуру, а не точні тексти повідомлень
pm.expect(body.items).to.be.an("array");
pm.expect(body.items.length).to.eql(0);
});
Зверніть увагу: ми не перевіряємо точний текст повідомлень, бо це часто найнестабільніша частина. Ми перевіряємо форму: масив є, він порожній, отже «нічого не знайдено» виражено саме так.
Ще один невеликий, але корисний чек — Content-Type. Іноді на помилках сервіс зривається і віддає HTML. Якщо ви очікуєте JSON, це варто знати.
pm.test("Відповідь — JSON", () => {
// Content-Type може бути відсутній, тому підстраховуємося порожнім рядком
const ct = pm.response.headers.get("Content-Type") || "";
// Фіксуємо очікування: хочемо JSON навіть у помилкових сценаріях (якщо провайдер так уміє)
pm.expect(ct).to.include("application/json");
});
Якщо цей тест раптово падає на 500 і text/html, ви хоча б розумієте: «ага, на внутрішніх помилках сервіс віддає не JSON, отже в клієнті це потрібно враховувати». Не сьогодні, не в коді, а просто як факт контракту.
І, нарешті, повертаємося до chained-сценарію. Якщо ви використовуєте bookId із пошуку, намагайтеся не залишати в середовищі старі значення, інакше negative-path стане фальшивим. Ми вже показували прийом unset перед установленням. Він простий, але рятує від ілюзії «усе працює».
7. Типові помилки negative-path у Postman
Помилка №1: вважати negative-path «будь-яким червоним екраном» і змішувати його з мережевими збоями.
Якщо Postman пише Could not get any response, це майже завжди не про контракт, а про те, що ви не отримали HTTP-відповідь. Це важливо вміти спостерігати, але це інша категорія проблем. Negative-path починається там, де є status code і хоч якась передбачувана поведінка API.
Помилка №2: обирати негативні значення, які «інколи існують».
Наприклад, ви берете ID навмання і сподіваєтеся, що його немає. А потім він раптом з’являється або провайдер змінює дані, і ваш negative-case раптово стає happy-case. Набагато краще завести missingBookId = DOES_NOT_EXIST або інший очевидний маркер і використовувати його як завідомо відсутній.
Помилка №3: перевіряти тільки body й ігнорувати status/headers.
Зовнішній сервіс може повернути той самий JSON, але з різними статусами, або повернути помилку взагалі без JSON. Якщо дивитися тільки на body, ви втрачаєте половину контракту. У negative-path насамперед дивіться на статус, потім Content-Type, і лише потім на форму body.
Помилка №4: ламати happy-path змінні negative-запитами.
Якщо ви перезаписуєте bookId у середовищі «для перевірки missing-id», ви потім самі собі створюєте головоломку: чому ланцюжок «пошук → деталі» перестав працювати. Розділяйте змінні за змістом: bookId — для happy chained flow, missingBookId — для negative details.
Помилка №5: перетворювати negative-path на архів разових експериментів.
Запит «порожній q» має сенс лише тоді, коли він оформлений як сценарій, названий за змістом і лежить поруч із happy-path. Інакше це не частина колекції, а просто ваш особистий слід на піску, який зникне за кілька днів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ