← Назад к вопросам

Как использовать fixture для работы с БД в тестировании?

2.0 Middle🔥 191 комментариев
#Базы данных (SQL)#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Fixtures в pytest для работы с БД

Fixtures — это функции в pytest, которые предоставляют тестам переиспользуемые данные и ресурсы. Для работы с БД они критичны для изоляции и воспроизводимости тестов.

1. Базовая fixture для БД

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.models import Base

# conftest.py

@pytest.fixture
def db_session():
    """Создаёт изолированную сессию БД для каждого теста"""
    # Используй тестовую БД (SQLite в памяти)
    engine = create_engine("sqlite:///:memory:///")
    Base.metadata.create_all(engine)
    
    SessionLocal = sessionmaker(bind=engine)
    session = SessionLocal()
    
    yield session
    
    session.close()
    Base.metadata.drop_all(engine)

# test_models.py

def test_create_user(db_session: Session):
    from app.models import User
    user = User(name="John", email="john@example.com")
    db_session.add(user)
    db_session.commit()
    
    assert db_session.query(User).count() == 1

2. Fixture с реальной БД (PostgreSQL)

Для интеграционных тестов используй реальную БД:

import os
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = os.getenv(
    "TEST_DATABASE_URL",
    "postgresql://user:password@localhost:5432/test_db"
)

@pytest.fixture(scope="session")
def db_engine():
    """Создаёт engine для всей сессии тестов"""
    engine = create_engine(DATABASE_URL)
    Base.metadata.create_all(engine)
    yield engine
    Base.metadata.drop_all(engine)

@pytest.fixture
def db_session(db_engine):
    """Создаёт транзакцию для каждого теста"""
    connection = db_engine.connect()
    transaction = connection.begin()
    
    Session = sessionmaker(bind=connection)
    session = Session()
    
    yield session
    
    session.close()
    transaction.rollback()  # Откатываем изменения после теста
    connection.close()

3. Fixture с подготовкой данных (factories)

Используй factory_boy для создания тестовых данных:

import pytest
from factory import Factory
from factory.sqlalchemy import SQLAlchemyModelFactory
from app.models import User, Post

class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = None  # Будет установлено в fixture
    
    name = "Test User"
    email = "test@example.com"

class PostFactory(SQLAlchemyModelFactory):
    class Meta:
        model = Post
        sqlalchemy_session = None
    
    title = "Test Post"
    author = None  # Свяжется с пользователем

@pytest.fixture
def factories(db_session):
    """Настраивает factories для работы с БД"""
    UserFactory._meta.sqlalchemy_session = db_session
    PostFactory._meta.sqlalchemy_session = db_session
    
    return {"user": UserFactory, "post": PostFactory}

def test_create_post_with_author(db_session, factories):
    user = factories["user"].create(name="Alice")
    post = factories["post"].create(author_id=user.id)
    
    assert post.author.name == "Alice"

4. Fixture для очистки данных между тестами

@pytest.fixture(autouse=True)
def cleanup_db(db_session):
    """Автоматически выполняется перед каждым тестом"""
    from app.models import User, Post, Comment
    
    yield  # Выполнить тест
    
    # Очистить данные после теста
    db_session.query(Comment).delete()
    db_session.query(Post).delete()
    db_session.query(User).delete()
    db_session.commit()

5. Fixture с моком для быстрых unit-тестов

Для unit-тестов используй mocks вместо реальной БД:

from unittest.mock import Mock, MagicMock
import pytest

@pytest.fixture
def mock_db_session():
    """Возвращает mock сессии БД для unit-тестов"""
    session = MagicMock(spec=Session)
    session.query.return_value.filter_by.return_value.first.return_value = None
    return session

def test_service_with_mock(mock_db_session):
    from app.services import UserService
    
    service = UserService(db=mock_db_session)
    result = service.get_user_by_email("test@example.com")
    
    # Проверить, что был сделан запрос
    mock_db_session.query.assert_called()

6. Fixture с параметризацией

Тестируй с разными наборами данных:

@pytest.fixture(params=[
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
])
def user_data(request):
    return request.param

def test_user_creation_multiple(db_session, user_data):
    from app.models import User
    user = User(**user_data)
    db_session.add(user)
    db_session.commit()
    
    assert user.email == user_data["email"]

7. Fixture с async для асинхронных операций

import pytest
from async_generator import asynccontextmanager

@pytest.fixture
async def async_db_session():
    """Fixture для async/await операций"""
    async with AsyncSession(engine) as session:
        yield session

@pytest.mark.asyncio
async def test_async_user_create(async_db_session):
    from app.models import User
    user = User(name="Async User", email="async@test.com")
    async_db_session.add(user)
    await async_db_session.commit()
    
    result = await async_db_session.execute(
        select(User).where(User.email == "async@test.com")
    )
    assert result.scalar_one().name == "Async User"

8. Scope и жизненный цикл fixtures

@pytest.fixture(scope="function")  # По умолчанию — для каждого теста
def fixture_function():
    return "created for each test"

@pytest.fixture(scope="class")  # Одна на весь класс тестов
def fixture_class():
    return "created for entire class"

@pytest.fixture(scope="module")  # Одна на модуль
def fixture_module():
    return "created for entire module"

@pytest.fixture(scope="session")  # Одна на всю сессию тестов
def fixture_session():
    return "created for entire session"

class TestUserClass:
    def test_one(self, fixture_class):
        assert fixture_class == "created for entire class"
    
    def test_two(self, fixture_class):
        # Используется один объект из первого теста
        assert fixture_class == "created for entire class"

9. Пример полной conftest.py

# conftest.py
import os
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker, Session
from app.models import Base
from app.config import settings

@pytest.fixture(scope="session")
def db_engine():
    engine = create_engine(
        os.getenv("TEST_DATABASE_URL", "sqlite:///:memory:///")
    )
    Base.metadata.create_all(engine)
    yield engine
    Base.metadata.drop_all(engine)

@pytest.fixture
def db_session(db_engine):
    connection = db_engine.connect()
    transaction = connection.begin()
    
    session = sessionmaker(bind=connection)()
    
    yield session
    
    session.close()
    transaction.rollback()
    connection.close()

@pytest.fixture
def client(db_session):
    """FastAPI TestClient с переопределённой БД"""
    from fastapi.testclient import TestClient
    from app.main import app
    from app.dependencies import get_db
    
    app.dependency_overrides[get_db] = lambda: db_session
    
    yield TestClient(app)
    
    app.dependency_overrides.clear()

Лучшие практики

  • Изоляция: каждый тест должен быть независимым
  • Очистка: откатывай транзакции после теста
  • Скорость: используй in-memory БД для unit-тестов
  • Читаемость: давай fixtures описательные имена
  • Переиспользование: помещай fixtures в conftest.py

Итог

Fixtures — это мощный инструмент для:

  • Создания изолированной БД
  • Подготовки тестовых данных
  • Очистки после тестов
  • Параметризации тестов
  • Управления жизненным циклом ресурсов
Как использовать fixture для работы с БД в тестировании? | PrepBro