Модуль 34

Тестування FastAPI (pytest, TestClient)

Ось готовий урок, написаний у стилі Девіда Малана: енергійний, зрозумілий, з акцентом на "чому" і "як", українською мовою.


🎓 CS50: Тестування FastAPI (pytest, TestClient)

Привіт, друзі! Радий бачити вас знову.

Сьогодні ми поговоримо про дещо, що відрізняє аматора, який пише код "аби працювало", від професійного інженера, який будує надійні системи.

1. 🔥 Вступ: Чому ми взагалі тут?

Уявіть ситуацію. Ви будуєте хмарочос. Ви поклали фундамент, звели стіни, вставили вікна. І ось, за тиждень до здачі об'єкта, ви вирішуєте замінити проводку на 40-му поверсі. Ви міняєте один кабель.

Питання: Чи впевнені ви, що після цього ліфт на першому поверсі все ще працює?

У програмуванні це відбувається щодня. Ви змінюєте функцію реєстрації користувача, і раптом "ламається" кошик покупок. Чому? Тому що системи взаємопов'язані.

Досі ми перевіряли наші API так: 1. Запускали сервер (uvicorn main:app --reload). 2. Відкривали Swagger UI (/docs). 3. Тицяли кнопки, вводили дані вручну. 4. Дивилися очима: "О, наче працює".

А тепер уявіть, що у вас 100 ендпоінтів. Ви будете перевіряти всі 100 вручну після кожної, навіть найменшої зміни коду? Звісно ні. Ви просто сподіватиметеся, що нічого не зламали. Але "надія" — це не інженерна стратегія.

Нам потрібен робот. Робот, який за секунду перевірить усі ваші ендпоінти й скаже: "Все чисто, капітане!" або "Агов, ти зламав логін!".

Цей робот — це автоматичні тести. І сьогодні ми навчимося їх писати для FastAPI.


2. 🧠 Теоретична база: Що "під капотом"?

Перш ніж писати код, давайте зрозуміємо механіку. Нам знадобляться два інструменти:

  1. pytest — це наш головнокомандувач. Це бібліотека, яка шукає всі файли з тестами, запускає їх і малює красиві зелені (успіх) або червоні (провал) звіти.
  2. TestClient — це наш "шпигун" від FastAPI.

Як працює TestClient? (Інтуїція)

Зазвичай, коли клієнт звертається до вашого API, запит йде через інтернет (або локальну мережу) -> на порт -> на сервер -> у ваш додаток.

TestClient обманює систему. Він бере ваш додаток (app) і звертається до нього напряму, оминаючи мережу, порти та запуск сервера. Він каже: "Привіт, додаток! Я нібито браузер, і я нібито надсилаю тобі GET-запит. Що б ти мені відповів?"

Чому це круто? * Швидкість: Це відбувається миттєво, бо немає мережевих затримок. * Ізоляція: Вам не треба запускати сервер (uvicorn) щоб прогнати тести.

Ключове поняття: assert

Єдине, що треба запам'ятати із синтаксису Python сьогодні — це слово assert (стверджувати).

x = 5
assert x == 5  # Все добре, програма йде далі
assert x == 10 # ПОМИЛКА! Тест падає!

Ми будемо робити запит до API, отримувати відповідь і стверджувати, що вона правильна.


3. 🧪 Приклади: Від Hello World до реальності

Давайте напишемо код. Уявіть, що у нас є файл main.py з простим API.

Крок 0: Наш "піддослідний" (main.py)

# main.py
from fastapi import FastAPI

app = FastAPI()

fake_db = {"apple": {"price": 10}, "banana": {"price": 5}}

@app.get("/")
def read_root():
    return {"msg": "Hello World"}

@app.get("/items/{item_name}")
def read_item(item_name: str):
    if item_name in fake_db:
        return {"item": item_name, "price": fake_db[item_name]["price"]}
    return {"error": "Item not found"}

Тепер створимо файл test_main.py. Зверніть увагу: pytest автоматично шукає файли, що починаються на test_.

Приклад 1: Перевірка "рукостискання"

Ми хочемо перевірити, чи працює кореневий маршрут /. Що ми очікуємо? Статус код 200 (OK) і JSON {"msg": "Hello World"}.

# test_main.py
from fastapi.testclient import TestClient
from main import app # Імпортуємо наш додаток

# Створюємо клієнта, передаючи йому наш додаток
client = TestClient(app)

def test_read_root():
    # 1. Дія: Робимо запит
    response = client.get("/")

    # 2. Перевірка: Статус код має бути 200
    assert response.status_code == 200

    # 3. Перевірка: Тіло відповіді має співпадати
    assert response.json() == {"msg": "Hello World"}

Питання до вас: Якби я змінив у main.py "Hello World" на "Hello Ukraine", а тест не змінив, що б сталося при запуску pytest? Відповідь: Тест би впав, і pytest показав би різницю: очікували World, отримали Ukraine.

Приклад 2: Перевірка логіки (Товар існує)

Давайте перевіримо /items/apple.

def test_read_item_apple():
    response = client.get("/items/apple")

    assert response.status_code == 200
    # Перевіряємо, чи ми отримали правильну ціну
    assert response.json() == {"item": "apple", "price": 10}

Приклад 3: Перевірка помилки (Товара немає)

А що, якщо користувач запитає pizza? Наш код повертає JSON з помилкою. Але статус код там все ще 200 (за замовчуванням), хоча логічно було б 404 (але в нашому простому коді ми це не налаштували, тому перевіряємо те, що є).

def test_read_item_missing():
    response = client.get("/items/pizza")

    assert response.status_code == 200
    assert response.json() == {"error": "Item not found"}

4. 🛠 Практична частина

Час забруднити руки кодом! Відкрийте свій редактор.

Встановіть необхідне: pip install fastapi uvicorn pytest httpx

Завдання 1: Репродукція Створіть main.py і test_main.py з кодом вище. Запустіть у терміналі команду pytest. Добийтеся зеленого кольору ("passed").

Завдання 2: Зламайте систему Змініть у main.py ціну яблука на 15. Запустіть pytest знову. Прочитайте помилку. Вона зрозуміла?

Завдання 3: Виправлення багу У main.py змініть логіку так, щоб якщо товару немає, повертався не просто JSON, а HTTP Exception 404. Підказка: raise HTTPException(status_code=404, detail="Item not found"). Тепер виправте тест test_read_item_missing, щоб він очікував status_code == 404.

Завдання 4: Нова функціональність Додайте в main.py ендпоінт, який додає новий товар (POST запит).

@app.post("/items/")
def create_item(name: str, price: int):
    fake_db[name] = {"price": price}
    return {"msg": "created"}

Напишіть для нього тест test_create_item. Підказка: використовуйте client.post("/items/", json={"name": "orange", "price": 20}).

Завдання 5: Кейс "Жадний менеджер" Менеджер каже, що ціна не може бути від’ємною. 1. Додайте валідацію в create_item (якщо price < 0, повернути помилку 400). 2. Напишіть тест, який пробує створити товар з ціною -50 і перевіряє, що система його НЕ створила (повернула 400).


5. 💡 Мислення як у розробника

Ви зараз можете подумати: "Ого, писати тести — це ж писати вдвічі більше коду!". Так, це правда. Але давайте подивимось, як думає Senior Developer:

  1. Тести — це інвестиція. Ви витрачаєте 10 хвилин зараз, щоб зекономити 5 годин дебаггингу вночі перед релізом.
  2. Тести — це документація. Якщо ви прийшли в новий проєкт і не розумієте, що робить цей дивний ендпоінт, подивіться його тести. Там показано, що в нього входить і що виходить.
  3. Не тестуйте бібліотеки. Не треба писати тест, щоб перевірити, чи правильно Python додає 2+2, або чи працює сам FastAPI. Тестуйте свою бізнес-логіку.

Типова помилка новачків: Писати один гігантський тест, який перевіряє все підряд. Правило: Один тест — один конкретний сценарій (успішна реєстрація, реєстрація з поганим паролем, реєстрація існуючого юзера — це 3 різні тести).


6. 🧩 Підсумок

Що ми сьогодні зробили? Ми перестали бути просто "кодерами" і стали трохи "інженерами з якості".

  • Ви знаєте, що таке pytest (запускач тестів).
  • Ви знаєте, що таке TestClient (симулятор запитів без інтернету).
  • Ви вмієте писати прості твердження (assert), щоб перевіряти статус і дані.

Тепер, коли ви вносите зміни в код, ви просто пишете pytest і бачите результат. Це дає неймовірне почуття спокою та впевненості.

Тизер наступної теми: У наших прикладах ми використовували fake_db — звичайний словник. Але в реальності у нас справжня база даних (PostgreSQL або SQLite). Як тестувати код, щоб тестові дані не засмічували справжню базу? Про тестові бази даних та фікстури (fixtures) ми поговоримо наступного разу.

А поки що... це був CS50! Кодіть відповідально! 💻🚀