Тестирование FastAPI через TestClient: практическое руководство
TestClient из Starlette позволяет тестировать эндпоинты FastAPI без запуска сервера. Разбираем рабочие приёмы: от простых GET-запросов до подмены зависимостей и тестирования WebSocket, с конкретными примерами и типичными граблями.

Зачем TestClient, если есть curl и Postman
Ручные запросы к API — это не тесты. Они не повторяются автоматически, не ловят регрессии и не работают в CI. TestClient из пакета Starlette (FastAPI построен поверх Starlette) решает задачу иначе: вы вызываете свои эндпоинты как обычные функции Python, получаете объект Response и проверяете его через assert. Сервер при этом не стартует — запросы проходят через ASGI-интерфейс напрямую.
Всё, что описано ниже, проверено для FastAPI 0.110+ и Python 3.11+. TestClient использует httpx под капотом начиная с версии Starlette 0.20; более ранние версии использовали requests — и API отличается.
Что нужно до начала
- Python 3.10+
- FastAPI 0.100+ (в более ранних версиях часть приёмов может не работать)
- pytest — стандартный раннер для примеров ниже
- httpx — устанавливается как зависимость Starlette, но убедитесь, что пакет присутствует:
pip install httpx
Установка, если проект начинается с нуля:
pip install "fastapi[standard]" pytest httpx
Минимальный тест за пять минут
Создадим приложение и тест в двух файлах. Файл main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
def ping():
return {"status": "ok"}
Файл test_main.py:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_ping_returns_ok():
response = client.get("/ping")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
Запуск: pytest test_main.py -v. В консоли увидите PASSED — значит, всё работает. Обратите внимание: импортируется не экземпляр Uvicorn, а сам объект app. Сервер не поднимается.
POST, заголовки, query-параметры
TestClient повторяет интерфейс httpx, поэтому вы передаёте данные так же, как в обычном HTTP-клиенте.
def test_create_item():
payload = {"name": "Клавиатура", "price": 4500}
response = client.post(
"/items",
json=payload,
headers={"X-Request-Id": "test-123"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Клавиатура"
assert "id" in data # эндпоинт должен вернуть сгенерированный id
Query-параметры передаются через params:
response = client.get("/items", params={"skip": 0, "limit": 10})
Подмена зависимостей через dependency_overrides
Здесь начинается самое полезное. FastAPI позволяет заменить любую зависимость (Depends) на фейковую прямо в тесте. Типичный случай — база данных: вы не хотите стучаться в PostgreSQL из CI.
Как это выглядит
Допустим, эндпоинт зависит от функции get_db:
# main.py
from fastapi import Depends, FastAPI
app = FastAPI()
def get_db():
# В реальном коде — сессия SQLAlchemy или другой коннект
raise RuntimeError("Нет соединения с базой")
@app.get("/users/{user_id}")
def read_user(user_id: int, db=Depends(get_db)):
# ... логика
return {"user_id": user_id}
Тест подменяет get_db:
# test_main.py
from fastapi.testclient import TestClient
from main import app, get_db
def fake_db():
return "fake_connection"
app.dependency_overrides[get_db] = fake_db
client = TestClient(app)
def test_read_user():
response = client.get("/users/42")
assert response.status_code == 200
assert response.json()["user_id"] == 42
Важно: dependency_overrides — словарь на уровне экземпляра app. Если один тест подменил зависимость, она остаётся подменённой для следующего. Очищайте в фикстуре:
import pytest
@pytest.fixture(autouse=True)
def reset_overrides():
yield
app.dependency_overrides.clear()
Тестирование асинхронных эндпоинтов
TestClient работает в синхронном контексте — и с async-эндпоинтами тоже. Starlette сама оборачивает вызов в event loop. Никакого await или pytest-asyncio для TestClient не нужно.
Однако если вам нужен именно асинхронный клиент (например, для тестов с реальной async-базой), используйте httpx.AsyncClient напрямую с параметром transport=ASGITransport(app=app). Это отдельная тема, здесь рассматриваем только синхронный TestClient.
Тестирование WebSocket
TestClient умеет работать с WebSocket-эндпоинтами через контекстный менеджер:
# main.py
@app.websocket("/ws")
async def websocket_endpoint(websocket):
await websocket.accept()
data = await websocket.receive_text()
await websocket.send_text(f"echo: {data}")
await websocket.close()
# test_main.py
def test_websocket_echo():
with client.websocket_connect("/ws") as ws:
ws.send_text("привет")
response = ws.receive_text()
assert response == "echo: привет"
Сравнение подходов к организации тестов
| Подход | Плюсы | Минусы | Когда выбирать |
|---|---|---|---|
| Клиент на уровне модуля | Один экземпляр, быстро | Общее состояние между тестами, нужна ручная очистка overrides | Маленький проект, десяток тестов |
| Клиент в pytest-фикстуре | Изоляция, автоматическая очистка | Чуть больше boilerplate | Средний и большой проект |
| httpx.AsyncClient + ASGITransport | Полностью асинхронный, ближе к реальному поведению | Нужен pytest-asyncio, сложнее настройка | Async-зависимости, реальная база в тестах |
Если тесты начинают влиять друг на друга — это почти всегда незачищенный dependency_overrides или мутабельное состояние в app. Фикстура с autouse и clear() решает 90% таких проблем.
Типичные ошибки и как их читать
Пять ситуаций, которые встречаются чаще всего:
- 422 Unprocessable Entity — вы передали тело запроса не через
json=, а черезdata=, или в payload отсутствует обязательное поле. Прочитайтеresponse.json()["detail"]— там Pydantic покажет, какое поле не прошло валидацию. - 404 Not Found — путь в тесте не совпадает с маршрутом. Частая причина: забыли префикс роутера (
/api/v1/...). - Stale dependency override — один тест подменил зависимость, следующий упал, потому что получил чужой фейк. Лечение: фикстура с
app.dependency_overrides.clear(). - ImportError при импорте app — если
main.pyпри импорте пытается подключиться к базе. Решение: вынесите создание app в отдельную функцию или используйте ленивую инициализацию. - RuntimeError: Use 'await' in async tests — вы пытаетесь использовать TestClient внутри async-функции теста. TestClient синхронный, используйте обычные
def test_....
Фикстура-шаблон для реального проекта
import pytest
from fastapi.testclient import TestClient
from main import app, get_db
@pytest.fixture()
def test_client():
"""Создаёт клиент с подменённой базой и чистит состояние после теста."""
def fake_db():
return "in_memory_stub"
app.dependency_overrides[get_db] = fake_db
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()
def test_list_items(test_client):
response = test_client.get("/items")
assert response.status_code == 200
assert isinstance(response.json(), list)
Конструкция with TestClient(app) as client вызывает события startup и shutdown приложения — это важно, если вы инициализируете ресурсы через lifespan.
Сноска: ASGI (Asynchronous Server Gateway Interface) — стандарт взаимодействия между Python-фреймворком и сервером. TestClient использует ASGI-интерфейс приложения напрямую, без сетевого уровня, поэтому запросы проходят в рамках одного процесса и не требуют открытого порта.
Что делать дальше
Синхронный TestClient закрывает большинство сценариев: CRUD, валидацию, авторизацию, WebSocket. Когда проект перерастёт этот уровень — переходите к httpx.AsyncClient с ASGITransport для полностью асинхронных тестов. Отдельно стоит разобраться с тестовыми базами: testcontainers-python или SQLite in-memory для SQLAlchemy — это следующий логический шаг. Мерьте покрытие через pytest --cov и следите за одной метрикой: не процентом строк, а количеством эндпоинтов без единого теста — именно они стреляют на проде.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: FastAPI — Testing (официальная документация), Starlette — Test Client (официальная документация)
Частые вопросы
TestClient работает с async-эндпоинтами?
Чем TestClient отличается от httpx.AsyncClient?
Как тестировать эндпоинты, защищённые авторизацией?
Что если у меня другая версия FastAPI и примеры не работают?
Тесты влияют друг на друга — результат зависит от порядка запуска. Что делать?
Можно ли тестировать загрузку файлов через TestClient?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.