FastAPI для начинающих: API с базой данных и тестамиТесты вместо ручного прокликиванияFixtures, временная база и dependency override

Fixtures, временная база и dependency override

Уроки курсаFixtures, временная база и dependency override

Override get_db: отдельная Session на каждый запрос и восстановление после теста

Теперь есть функция create_db_engine, которую можно вызвать с путём к временному файлу. Осталось сказать FastAPI: «для тестов не вызывай get_db из database.py — вместо неё используй вот эту функцию». Это делает app.dependency_overrides.

Создайте рядом с main.py файл conftest.py — pytest автоматически загружает его перед тестами и делает все объявленные в нём fixtures доступными без явного импорта.

# conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import sessionmaker
from main import app
from database import create_db_engine, get_db
from models import Base

@pytest.fixture
def client(tmp_path):
    database_path = tmp_path / "test.db"
    test_engine = create_db_engine(f"sqlite:///{database_path.as_posix()}")
    previous_overrides = dict(app.dependency_overrides)
    try:
        Base.metadata.create_all(bind=test_engine)
        TestingSession = sessionmaker(bind=test_engine, autocommit=False, autoflush=False)

        def override_get_db():
            db = TestingSession()
            try:
                yield db
            finally:
                db.close()

        app.dependency_overrides[get_db] = override_get_db
        with TestClient(app) as test_client:
            yield test_client
    finally:
        app.dependency_overrides.clear()
        app.dependency_overrides.update(previous_overrides)
        test_engine.dispose()
        database_path.unlink(missing_ok=True)

tmp_path — встроенная fixture pytest: она даёт отдельный временный каталог для каждого теста. Файл test.db внутри этого каталога живёт ровно столько, сколько нужно тесту.

previous_overrides = dict(app.dependency_overrides) сохраняет снимок текущего словаря. Это важно: если в проекте уже есть другие подмены (например, для конфигурации), их нельзя потерять и нельзя удалить чужой подменой. В finally словарь восстанавливается двумя строками: сначала очищается, потом заполняется сохранённым снимком.

app.dependency_overrides[get_db] = override_get_db — ключ здесь именно объект функции get_db, тот же самый, который передаётся в Depends внутри обработчиков. Строка "get_db" или вызов get_db() не сработают.

override_get_db открывает новую Session на каждый входящий запрос. Все запросы одного теста видят один и тот же файл базы, но каждый запрос получает свою Session — так же, как в боевом get_db.

Порядок завершения в finally важен: сначала завершается контекст TestClient, потом восстанавливаются overrides, потом test_engine.dispose() закрывает пул соединений engine, и только после этого удаляется файл. На Windows открытый файл базы нельзя удалить — dispose обязателен перед unlink.

Тесты

# test_rooms.py
def test_create_room(client):
    response = client.post("/api/rooms", json={"name": "Альфа", "capacity": 4})
    assert response.status_code == 201
    room = response.json()
    assert type(room["id"]) is int
    assert room["name"] == "Альфа"
    assert room["capacity"] == 4
    response = client.get("/api/rooms")
    assert response.status_code == 200
    assert response.json() == [room]

def test_empty_rooms(client):
    response = client.get("/api/rooms")
    assert response.status_code == 200
    assert response.json() == []

test_create_room создаёт комнату и сразу проверяет, что GET возвращает её. test_empty_rooms проверяет, что база пуста. Эти два теста проходят в любом порядке: каждый получает свою базу от своего запуска fixture, и запись из одного теста физически не существует в файле другого.

Запуск из папки testing_demo в том же окружении, где установлены pytest и httpx:

python -m pytest -q test_rooms.py

Оба теста пройдут. Файл roomly_intervals.db не появится. Если он существовал до запуска тестов, его содержимое останется нетронутым.

Два независимых запуска fixture client со scope function. В тесте A POST создаёт запись, а последующий GET того же теста видит её. В тесте B первый GET возвращает пустой список. Оба файла называются test.db, но находятся в разных tmp_path; обычная база проекта не открывается. Различие обеспечивают отдельный engine и override get_db, а не само создание TestClient.
Запросы одного теста используют его временную базу. Другой тест получает новый файл через отдельный запуск fixture.