Обробка помилок — один із наріжних каменів якісного API. Користувачі, що стикаються з непередбачуваними помилками (наприклад, "500 Internal Server Error"), втрачають довіру до додатку. Розробникам же важливо, щоб такі помилки були не лише зрозумілими для користувача, а й простими для налагодження.
FastAPI надає зручні інструменти для генерації та обробки помилок. За допомогою стандартних виключень, таких як HTTPException, можна повертати детальні, правильно відформатовані відповіді з помилками.
Загальні принципи обробки помилок:
- Зрозумілість: повідомлення про помилки мають бути зрозумілими і містити тільки ту інформацію, яка важлива для користувача (будь-якої зайвої інформації — "до побачення").
- Послідовність: весь API має повертати помилки в одному форматі.
- Інформативність: для розробників важливо, щоб помилки містили максимум корисної інформації.
Типи помилок, які слід тестувати
Ваш додаток може стикатися з різними сценаріями помилок:
- Клієнтські помилки (4xx HTTP Status Code): некоректний запит, невідповідність даних, відсутні параметри тощо.
- Серверні помилки (5xx HTTP Status Code): несподівані збої, відсутні залежності, помилки в логіці сервера.
- Користувацькі виключення. Наприклад, бізнес-логіка, специфічна тільки для вашого додатку (нестача коштів, невірний пароль, перевищення лімітів).
Давайте розбиратись, як писати тести для обробки всіх цих випадків.
Генерація та обробка помилок у FastAPI
FastAPI робить обробку помилок такою простою, що у розробників виступають сльози радості (навіть у тих, хто вже "випробував все"). Основним інструментом є HTTPException, який дозволяє повертати HTTP-відповіді з кодами помилок.
Приклад обробки помилок у FastAPI
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id <= 0:
# Генеруємо виключення при некоректному ID
raise HTTPException(status_code=400, detail="Item ID must be positive.")
# Повернемо фейковий об'єкт
return {"item_id": item_id, "name": "Test Item"}
У цьому прикладі при запиті з некоректним item_id (наприклад, від'ємне значення) ми повертаємо 400 Bad Request з описом помилки.
Тести для обробки помилок
Почнемо, нарешті, з головного — написання тестів для обробки помилок. Для цього нам знадобиться використати TestClient з FastAPI.
Тестування клієнтських помилок (4xx)
Почнемо з тестування нашого ендпоінта /items/{item_id} на випадок некоректного item_id.
from fastapi.testclient import TestClient
from main import app # Підключаємо наш додаток
client = TestClient(app)
def test_read_item_invalid_id():
# Від'ємний item_id викликає помилку 400
response = client.get("/items/-1")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID must be positive."}
Тут ми перевіряємо, що при передачі від'ємного item_id API повертає коректний HTTP-статус (400) і правильне повідомлення про помилку.
Тестування серверних помилок (5xx)
Уявімо, що в нашому додатку сталася несподівана помилка, наприклад, розрив з'єднання з базою даних. Ми хочемо переконатися, що такі сценарії коректно обробляються.
Приклад ендпоінта з штучним викликом помилки 500:
@app.get("/simulate_server_error")
async def simulate_error():
raise ValueError("Something went horribly wrong!")
Напишемо тест:
def test_server_error():
response = client.get("/simulate_server_error")
assert response.status_code == 500
# Перевіряємо, що помилка оброблена у форматі FastAPI
assert "detail" in response.json()
Тестування користувацьких виключень
Іноді помилки пов'язані з порушенням бізнес-логіки. Наприклад, при спробі перевести більше грошей, ніж є на рахунку користувача.
Ендпоінт для переказу грошей:
from fastapi import HTTPException
@app.post("/transfer")
async def transfer_funds(amount: float):
if amount > 1000.0: # Логіка
raise HTTPException(
status_code=400,
detail="Transfer amount exceeds the limit of $1000.00",
)
return {"status": "success", "transferred": amount}
Тестуємо перевищення ліміту переказу:
def test_transfer_amount_exceeds_limit():
response = client.post("/transfer", json={"amount": 1500.0})
assert response.status_code == 400
assert response.json()["detail"] == "Transfer amount exceeds the limit of $1000.00"
Обробка помилок через глобальні обробники
FastAPI дозволяє централізовано обробляти виключення за допомогою декоратора @app.exception_handler. Це корисно для обробки помилок, які можуть виникати в різних частинах додатку.
Налаштування глобального обробника помилок
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return JSONResponse(
status_code=422,
content={"detail": "Input validation failed", "errors": exc.errors()},
)
Тепер всі помилки валідації автоматично повертатимуть уніфіковану відповідь.
Тестування глобальних обробників
Якщо ви додаєте глобальні обробники, обов'язково тестуйте їхню роботу.
Приклад тесту для кастомного обробника валідації:
def test_validation_error():
response = client.get("/items/string_instead_of_int")
assert response.status_code == 422
assert response.json()["detail"] == "Input validation failed"
Особливості та типові помилки при тестуванні помилок
Іноді в процесі тестування помилок можна натрапити на труднощі:
- Неправильні очікування статусів. Пам'ятайте, що API має повертати ПРАВИЛЬНІ коди статусів. Наприклад, помилка валідації даних повертає 422, а не 400.
- Невідповідність повідомлень. Переконайтеся, що текст повідомлення про помилку збігається з тим, що очікує тест.
- Пропущені кейси. Завжди тестуйте як успішні сценарії, так і помилки. Наприклад, що станеться, якщо передати порожнє тіло POST-запиту?
Як це застосувати на практиці?
Добре протестоване API:
- Викликає менше сюрпризів при передачі коду іншим розробникам.
- Створює позитивне враження у користувачів завдяки зрозумілим і передбачуваним помилкам.
- Проходить перевірки якості на співбесідах і в процесі рев'ю коду.
І так, хто не тестує обробку помилок, той одного дня побачить у продакшені напис "Oops... Something Bad Happened". Краще цього уникнути.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ