Тестування API: що перевіряти, крім статус-коду
Повний чекліст для перевірки REST API: контракт, валідація, права доступу, ідемпотентність, пагінація і поведінка при помилках. З прикладами на pytest і httpx.
assert response.status_code == 200 — найпопулярніша і найменш корисна перевірка API. Сервер може повернути 200 з порожнім тілом, з чужими даними або з "price": "NaN". Розберімо, що насправді варто перевіряти.
1. Контракт відповіді
Структура, типи та обов'язкові поля. Найзручніше — описати схему й перевіряти кожну відповідь:
from pydantic import BaseModel, Field
from datetime import datetime
class Order(BaseModel):
id: int
status: str = Field(pattern="^(new|paid|shipped|cancelled)$")
total: float = Field(ge=0)
currency: str = Field(min_length=3, max_length=3)
created_at: datetime
def test_get_order_contract(api, order_id):
r = api.get(f"/orders/{order_id}")
assert r.status_code == 200
Order.model_validate(r.json()) # впаде з точним описом, яке поле не такПорадаЯкщо в команді є OpenAPI-специфікація, генеруйте перевірки з неї. Тоді тест ловить розбіжність між документацією і реальністю — а це окремий клас багів.
2. Валідація вхідних даних
Застосовуйте ті самі класи еквівалентності та граничні значення, що й для UI — тільки тепер можна надіслати те, чого форма не дозволить:
| Що надіслати | Очікування |
|---|---|
| Відсутнє обов'язкове поле | 400/422 + назва поля в помилці |
Неправильний тип ("quantity": "два") |
400/422 |
Межі: quantity = 0, 1, max, max+1 |
422 для невалідних |
Зайве поле ("is_admin": true) |
Ігнорується або 400 — але не застосовується |
| Дуже довгий рядок (100 000 символів) | 413/422, а не 500 |
| Невалідний JSON | 400, а не 500 |
Будь-яка відповідь 500 на некоректний ввід — це баг: сервер має перевіряти дані, а не падати.
3. Автентифікація й авторизація
Найдорожчі баги API — це доступ до чужих даних (IDOR).
def test_cannot_read_other_users_order(api_as_alice, order_of_bob):
r = api_as_alice.get(f"/orders/{order_of_bob}")
assert r.status_code in (403, 404)
def test_requires_auth(anonymous_api, order_id):
assert anonymous_api.get(f"/orders/{order_id}").status_code == 401Перевіряйте кожен метод окремо: часто GET захищений, а PATCH або DELETE на тому ж ресурсі — ні.
УвагаЗвичайний користувач не повинен змінювати поля, які йому не належать:
role,price,owner_id,status. Спробуйте надіслати їх уPATCHі перевірте, що в базі нічого не змінилось.
4. Коди помилок і тіло помилки
Клієнту потрібен не лише код, а й зрозуміла причина:
{
"error": "validation_failed",
"message": "Кількість має бути від 1 до 99",
"field": "quantity"
}Перевіряйте, що формат помилок однаковий на всіх ендпоінтах і що в них немає стектрейсів, SQL-запитів і внутрішніх шляхів.
5. Ідемпотентність і повтори
Мережа ненадійна: клієнт може надіслати той самий запит двічі.
PUTіDELETEза визначенням ідемпотентні: повторний виклик не змінює результат.POST /paymentsз тим самимIdempotency-Keyне повинен створити два платежі.
def test_payment_is_idempotent(api):
headers = {"Idempotency-Key": "test-7f3a"}
first = api.post("/payments", json={"order_id": 42, "amount": 500}, headers=headers)
second = api.post("/payments", json={"order_id": 42, "amount": 500}, headers=headers)
assert first.json()["id"] == second.json()["id"]
assert count_payments(order_id=42) == 16. Списки: пагінація, фільтри, сортування
- перша сторінка, остання, сторінка за межами (
page=9999→ порожній список, а не 500); limit= 0, 1, max, max+1;- стабільний порядок: елемент не з'являється на двох сторінках і не пропадає;
- фільтр не повертає чужих даних.
7. Побічні ефекти
Відповідь 201 ще не означає, що все добре. Перевірте наслідки: запис у БД, повідомлення в черзі, лист у тестовій поштовій скриньці, зміну залишку на складі.
Структура набору тестів
tests/api/
conftest.py # клієнти для різних ролей, фабрики тестових даних
test_orders_contract.py
test_orders_validation.py
test_orders_access.py
test_payments_idempotency.pyКожен тест створює свої дані і не залежить від порядку запуску. Тоді їх можна ганяти паралельно — і набір з сотень перевірок займає хвилину.
API повертає 404 замість 403, коли користувач запитує чуже замовлення. Це баг?
Не обов'язково. Багато API свідомо відповідають 404, щоб не розкривати сам факт існування ресурсу. Головне — щоб дані не повернулися і поведінка була однаковою для всіх методів. Варто звірити з вимогами до API.
Збережіть статтю собі
Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.
Увійти через Google Інші способи входу