Testing with httpx

FastAPI интегрируется с starlette.testclient.TestClient, который под капотом — синхронный httpx. Для настоящих async-тестов есть httpx.AsyncClient + ASGITransport.

Базовый тест

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_root():
    r = client.get("/")
    assert r.status_code == 200
    assert r.json() == {"message": "hello"}

Override зависимостей

Не поднимать настоящую БД ради тестов — подменить через dependency_overrides:

from app.deps import get_db
from app.main import app

def fake_db():
    return FakeSession()

app.dependency_overrides[get_db] = fake_db

def test_items():
    r = client.get("/items")
    assert r.status_code == 200

app.dependency_overrides — глобальный dict; чистить в фикстуре с yield.

Async-тесты

TestClient сам гоняет async через anyio. Если нужен реальный async-context (например, проверка BackgroundTasks):

import httpx
import pytest

@pytest.mark.anyio
async def test_async():
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac:
        r = await ac.get("/")
    assert r.status_code == 200

Фикстуры pytest

import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.deps import get_db

@pytest.fixture
def client():
    app.dependency_overrides[get_db] = lambda: FakeSession()
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()

def test_signup(client):
    r = client.post("/signup", json={"email": "a@b.c"})
    assert r.status_code == 200

Покрытие

  • 200/4xx/5xx для каждого эндпоинта хотя бы по одному
  • Валидация 422 на невалидных payload
  • Кастомные валидаторы — отдельный тест на каждый raise
  • Override зависимостей в каждом тесте, чтобы тесты не лезли в реальные ресурсы

Чеклист

  • Каждый эндпоинт покрыт хотя бы happy-path тестом
  • Невалидные payload → 422, не 500
  • БД и внешние API подменены через dependency_overrides
  • app.dependency_overrides.clear() после каждого теста
  • Тесты запускаются быстро (<1с на тест) — иначе никто не будет их гонять

Связанные заметки