← Назад к вопросам
Как использовать 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 — это мощный инструмент для:
- Создания изолированной БД
- Подготовки тестовых данных
- Очистки после тестов
- Параметризации тестов
- Управления жизненным циклом ресурсов