Тесты FastAPI: как проверить контракт, а не только статус 200
У API есть тест, который отправляет GET /health и получает 200. Сборка зелёная, но создание ресурса возвращает неправильный статус, а ошибочный POST добавляет пустую запись. Проверка доступности приложения полезна, однако она не подтверждает договор конкретной операции.
Выберите один законченный сценарий
Возьмём каталог комнат: создать комнату, прочитать её, получить 404 для отсутствующей и 422 для неправильного документа. Эти действия описывают наблюдаемое поведение, которым пользуется клиент. В тесте важно сохранить id из ответа создания и передать его в следующий запрос. Заранее заданный id способен скрыть ошибку результата POST.
Проверяйте и статус, и содержимое. 201 означает создание, но не подтверждает правильное имя. Полное равенство ожидаемому JSON замечает потерянное или лишнее поле. Для ошибки формы обычно достаточно типа и пути ошибки: полный английский текст сообщения менее стабилен и редко нужен интерфейсу.
Пример с реальной HTTP-валидацией
Установите FastAPI и HTTPX. Словарь здесь служит небольшим хранилищем, чтобы тест оставался самодостаточным. В проекте тот же сценарий нужно выполнить с временной базой: именно она подтверждает сохранение и ограничения хранения.
Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
rooms = {}
class RoomIn(BaseModel):
name: str = Field(min_length=1)
@app.post("/rooms", status_code=201)
def create(data: RoomIn):
room_id = len(rooms) + 1
rooms[room_id] = {"id": room_id, "name": data.name}
return rooms[room_id]
@app.get("/rooms/{room_id}")
def read(room_id: int):
if room_id not in rooms:
raise HTTPException(404, "room not found")
return rooms[room_id]
with TestClient(app) as client:
created = client.post("/rooms", json={"name": "Blue"})
assert created.status_code == 201
room = created.json()
assert room == {"id": 1, "name": "Blue"}
assert client.get(f"/rooms/{room['id']}").json() == room
before = dict(rooms)
invalid = client.post("/rooms", json={"name": ""})
assert invalid.status_code == 422
assert invalid.json()["detail"][0]["loc"] == ["body", "name"]
assert rooms == before
assert client.get("/rooms/999").status_code == 404
print("create/read/validation/missing-resource contract checked")Почему проверяется состояние после отказа
Ответ 422 должен означать, что команда не выполнена. Сравнение состояния до и после запроса делает это свойство наблюдаемым. Для настоящей базы лучше прочитать данные новой Session, а не доверять Python-объектам из предыдущей операции. Если бизнес-ошибка возникает после части изменений, отдельный тест должен проверить rollback.
Не подменяйте в таком тесте сам обработчик или Pydantic-модель. Иначе запрос пройдёт по другому пути и зелёный результат ничего не скажет о реальном контракте. Подменять внешнюю сеть или источник данных можно через явную зависимость, сохранив маршрут, разбор входа и сериализацию ответа.
Как перенести сценарий в pytest
Разделите независимые случаи на отдельные функции, а создание приложения и клиента вынесите в fixture. Данные для каждого теста должны начинаться с известного состояния. Тогда тест ошибки можно запустить отдельно, а его результат не зависит от того, создавал ли соседний тест комнату с номером один.
Оставьте один сквозной сценарий, где id действительно проходит из ответа POST в GET. Он связывает операции так, как это делает клиент. Дополните его точечными тестами значимых ошибок и границ; десятки одинаковых happy-path запросов обычно менее полезны, чем один тест запрета чужого ресурса или неудачной записи.
Следующая практика
В открытом уроке первого теста API вы выполните запрос через TestClient и проверите ответ. Развивайте его в связанный сценарий создания и чтения, сохраняя id из ответа и проверяя состояние после отказа. Откройте бесплатную практику TestClient и pytest.
Бесплатный курс FastAPI развивает тот же backend от первого маршрута до базы и авторизации. Продолжите программу, расширяя сценарий теста вместе с настоящим поведением приложения.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
