Skip to main content
Back to course
Pytest с нуля: тесты, которые реально работают
7 / 1164%

🔄 Fixtures: создаём данные один раз, используем везде

35 минут

⚠️ Урок устарел. Свежая версия в курсе 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 redis

Yield: 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.py

tests/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", и тесты влияют друг на друга. Что делать?

💡 Ответы
  1. Сделать фабрику пользователей:
@pytest.fixture
def user_factory():
    def _create(role="user"):
        return User(role=role)
    return _create
  1. Использовать yield для cleanup:
@pytest.fixture
def temp_file():
    path = create_temp_file()
    yield path
    path.unlink()
  1. Сменить scope или очищать данные:
@pytest.fixture(scope="function")
def clean_database():
    db = Database()
    yield db
    db.clear_all_data()

Чеклист качественных фикстур

  • Устраняют дублирование — код не повторяется в тестах
  • Правильный scope — начинайте с function, расширяйте при необходимости
  • Гарантированный cleanup — yield/finally для ресурсов
  • Изолированное состояние — тесты не влияют друг на друга
  • Понятные имена — сразу ясно, что возвращает фикстура
  • Общие фикстуры в conftest.py — переиспользование между файлами

Поздравляю! Теперь ваши тесты чище, надёжнее и проще в поддержке. 🎯