🔄 Fixtures: создаём данные один раз, используем везде
⚠️ Урок устарел. Свежая версия в курсе pytest-basics: Fixtures: создаём данные один раз.
Фикстуры: профессиональная организация тестов
🔄 До: 50+ строк дублирования, ручной cleanup, 15 минут на рефакторинг 10 тестов
✅ После: 0 дублирования, автоматический cleanup, 2 минуты на изменение во всех тестах
✅ Устраняем дублирование — данные создаются один раз, используются везде
✅ Гарантируем cleanup — ресурсы освобождаются даже при падении тестов
✅ Упрощаем поддержку — меняем в одном месте, а не в 20 тестах
Проблема: дублирование кода
# ❌ ДУБЛИРОВАНИЕ: одинаковый код в каждом тесте
def test_add_to_empty_list():
tasks = [] # Дублирование
result = add_task(tasks, "task1") # Дублирование
assert result == ["task1"]
def test_add_to_existing_list():
tasks = ["existing"] # Дублирование
result = add_task(tasks, "new") # Дублирование
assert result == ["existing", "new"]
def test_task_priority():
tasks = [] # Дублирование
task = add_task(tasks, "important", priority=10) # Дублирование
assert task.priority == 10
# 50+ строк однотипного setup в реальных проектахРешение: фикстуры убирают дублирование
import pytest
# ✅ ФИКСТУРЫ: переиспользуемые компоненты
@pytest.fixture
def empty_tasks():
return []
@pytest.fixture
def sample_tasks():
return ["task1", "task2"]
@pytest.fixture
def task_with_high_priority():
return Task("important", priority=10)
def test_add_to_empty_list(empty_tasks):
result = add_task(empty_tasks, "task1")
assert len(result) == 1
def test_add_to_existing_list(sample_tasks):
result = add_task(sample_tasks, "new task")
assert result == ["task1", "task2", "new task"]
def test_task_priority(task_with_high_priority):
assert task_with_high_priority.priority == 10Скоупы: контролируем время жизни
import pytest
@pytest.fixture(scope="function") # 🚀 По умолчанию: новый для каждого теста
def fresh_database():
db = Database()
db.connect()
yield db
db.disconnect()
@pytest.fixture(scope="class") # 🎯 Один раз на класс тестов
def api_client():
client = APIClient()
client.authenticate()
return client
@pytest.fixture(scope="module") # 🏗️ Один раз на файл
def docker_container():
container = start_postgres_container()
yield container
container.stop()
@pytest.fixture(scope="session") # 🌐 Один раз на всю сессию
def redis_connection():
redis = Redis()
redis.flushall()
return redisYield: setup и teardown вместе
yield — это «return + cleanup в одном месте».
Setup — всё, что до yield: создаём файл, открываем соединение, запускаем контейнер.
Teardown — всё, что после yield: удаляем файл, закрываем соединение, останавливаем контейнер.
Teardown выполнится даже если тест упал — идеальный способ гарантировать освобождение ресурсов.
import pytest
from pathlib import Path
import shutil
@pytest.fixture
def temporary_file():
path = Path("/tmp/test.txt")
path.write_text("test data") # Setup
yield path
path.unlink() # Teardown
@pytest.fixture
def database_transaction():
conn = create_connection()
tx = conn.begin()
try:
yield conn
finally:
tx.rollback() # 🛡️ Выполнится даже при падении теста
conn.close()Реалистичные примеры фикстур
Эти примеры показывают, как фикстуры решают реальные задачи: файловая конфигурация, тестовые данные в БД, моки внешних сервисов. Каждая фикстура делает setup один раз и отдаёт готовый объект тесту.
import pytest
from unittest.mock import patch
# Работа с файлами: создаём конфиг во временной директории
@pytest.fixture
def config_file(tmp_path):
config_path = tmp_path / "config.yaml"
config_path.write_text("debug: true\nport: 8000")
return config_path
# Работа с БД: создаём пользователя и отдаём его id
@pytest.fixture
def test_user(db_connection):
user_id = db_connection.execute(
"INSERT INTO users (email) VALUES (?) RETURNING id",
("test@example.com",)
).fetchone()[0]
return user_id
# Моки внешних сервисов: подменяем платежный шлюз
@pytest.fixture
def mock_payment_gateway():
with patch("app.services.payment_gateway.charge") as mock:
mock.return_value = {"status": "success", "id": "ch_123"}
yield mockФабрики, autouse и зависимости
Фабрики дают гибкие тестовые данные, autouse включает настройку для всех тестов без явного упоминания, зависимости позволяют строить фикстуры друг на друге.
import pytest
# ✅ Фабрика для гибкого создания данных
@pytest.fixture
def task_factory():
def _create_task(text="default task", priority=1, done=False):
return Task(text=text, priority=priority, done=done)
return _create_task
# ✅ Автоиспользование для глобальной настройки без явного указания в тестах
@pytest.fixture(autouse=True)
def reset_settings():
original_debug = settings.DEBUG
settings.DEBUG = True
yield
settings.DEBUG = original_debug
# ✅ Зависимость фикстур: user строится на базе database
@pytest.fixture
def database():
return Database()
@pytest.fixture
def user(database):
return database.create_user("test")conftest.py: общие фикстуры
Иерархия conftest наглядно:
tests/
├── conftest.py # 🌍 Доступно всем тестам
├── unit/
│ ├── conftest.py # 🚀 Только unit
│ └── test_todo.py
└── integration/
├── conftest.py # 🏗️ Только integration
└── test_database.pyСтруктура:
tests/
unit/
test_todo.py
test_user.py
integration/
test_database.py
conftest.pytests/conftest.py:
import pytest
from src.app import create_app
from src.database import Database
@pytest.fixture
def app():
"""Тестовое приложение"""
return create_app(testing=True)
@pytest.fixture
def client(app):
"""HTTP клиент"""
return app.test_client()
@pytest.fixture
def db_session():
"""Сессия БД с автоочисткой"""
db = Database(testing=True)
db.create_tables()
yield db
db.drop_tables()conftest.py — стандартное имя, которое pytest находит автоматически. Любые фикстуры внутри доступны всем тестам в этой папке и вложенных, без явного импорта. Это соглашение, но для автоподхвата фикстур использовать другое имя нельзя.
Практика
Попробуйте решить задачи сами, а затем сверяйтесь с примерами ниже.
- Задача 1: Устраните дублирование в тестах БД (автоматическое подключение/отключение, cleanup).
- Задача 2: Создайте фабрику для задач, чтобы легко менять текст/приоритет/статус без копипасты.
Продвинутые паттерны и решения
Это готовое решение задачи «Создайте фабрику для тестовых данных».
@pytest.fixture
def task_factory():
def _create_task(text="default task", priority=1, done=False):
return Task(text=text, priority=priority, done=done)
return _create_task
def test_task_creation(task_factory):
task = task_factory(text="Learn pytest", priority=1, done=False)
assert task.text == "Learn pytest"
def test_task_completion(task_factory):
task = task_factory(text="Write tests", priority=5, done=False)
task.mark_done()
assert task.done is True
def test_task_high_priority(task_factory):
task = task_factory(text="Critical", priority=10)
assert task.priority == 10
def test_task_defaults(task_factory):
task = task_factory() # Используем значения по умолчанию
assert task.text == "default task"Фабрика = один «конструктор данных» вместо множества отдельных фикстур: меняем аргументы — получаем нужный объект без копипасты.
Решение практики для БД
import pytest
@pytest.fixture
def database():
db = Database()
db.connect()
try:
yield db
finally:
db.disconnect() # 🛡️ Гарантированное отключение
def test_create_user(database):
user_id = database.create_user("alice")
assert user_id == 1
def test_delete_user(database):
database.create_user("alice")
database.delete_user("alice")
assert not database.user_exists("alice")
# 🎯 Убрали дублирование и добавили cleanup💥 Реальные последствия анти-паттернов
import pytest
shared_users = []
@pytest.fixture(scope="session")
def global_users():
return shared_users
def test_user_creation(global_users):
global_users.append("alice") # 🚨 Меняем глобальное состояние
def test_user_count(global_users):
assert len(global_users) == 0 # 💥 Падает из-за предыдущего теста- Недетерминированные тесты: проходят/падают в зависимости от порядка
- Сложно отладить: 100 тестов — непонятно, кто меняет состояние
- Flaky в CI/CD: иногда проходят, иногда нет
🚀 Шпаргалка: фикстуры в действии
Когда использовать каждый scope
- function — тестовые данные, моки, изолированные объекты
- class — общая конфигурация для группы тестов
- module — тяжёлые ресурсы (Docker, тестовая БД)
- session — кеш, глобальные конфиги, подключения к внешним сервисам
Паттерны для частых сценариев
@pytest.fixture
def sample_data():
return {"key": "value"}
@pytest.fixture
def temp_db():
db = setup()
yield db
db.cleanup()
@pytest.fixture
def user_factory():
defaults = {"name": "Test", "email": "test@example.com"}
return lambda **kwargs: User(**{**defaults, **kwargs})
@pytest.fixture(autouse=True)
def test_mode():
settings.TESTING = True
yield
settings.TESTING = FalseКоманды для отладки фикстур
pytest --fixtures # показать все фикстуры
pytest tests/test_example.py --fixtures # фикстуры файла
pytest -v -s tests/test_example.py::test_name # вывод во время тестаПроверьте понимание
Вопрос 1: В 10 тестах дублируется создание пользователя с разными ролями. Как улучшить?
Вопрос 2: Тесты работают с временными файлами, но иногда файлы не удаляются. В чём проблема?
Вопрос 3: Фикстура БД использует scope="session", и тесты влияют друг на друга. Что делать?
💡 Ответы
- Сделать фабрику пользователей:
@pytest.fixture
def user_factory():
def _create(role="user"):
return User(role=role)
return _create- Использовать
yieldдля cleanup:
@pytest.fixture
def temp_file():
path = create_temp_file()
yield path
path.unlink()- Сменить scope или очищать данные:
@pytest.fixture(scope="function")
def clean_database():
db = Database()
yield db
db.clear_all_data()Чеклист качественных фикстур
- Устраняют дублирование — код не повторяется в тестах
- Правильный scope — начинайте с function, расширяйте при необходимости
- Гарантированный cleanup — yield/finally для ресурсов
- Изолированное состояние — тесты не влияют друг на друга
- Понятные имена — сразу ясно, что возвращает фикстура
- Общие фикстуры в conftest.py — переиспользование между файлами
Поздравляю! Теперь ваши тесты чище, надёжнее и проще в поддержке. 🎯