yield fixture в pytest: как не потерять очистку при ошибке

Фикстура создаёт временную рабочую область, затем загружает настройки. Если настройки неверны, до yield выполнение не доходит. Код очистки, написанный просто после yield, в таком сценарии не запускается.

Где начинается ответственность за ресурс

После успешного создания ресурса очистка должна быть уже зарегистрирована. В yield-фикстуре pytest выполняет код после yield, когда тест закончен. Но если исключение возникло раньше yield, эта часть генератора не исполняется. Для уже открытого ресурса нужна защита обычным try/finally либо контекстным менеджером, установленная сразу после получения.

Ниже ресурс изображён небольшим объектом Session с наблюдаемым состоянием closed. Это позволяет проверить именно порядок действий. Для файла роль close выполняет файловый объект, для соединения — соответствующий клиент. Конкретное освобождение меняется, правило владения остаётся тем же.

Проверить оба пути без сломанной сессии pytest

Генератор build сначала создаёт Session, затем проверяет конфигурацию. Обычная фикстура делегирует ему управление через yield from. Отдельный тест вызывает тот же генератор с ошибочной конфигурацией и проверяет закрытие. Так ошибка подготовки проверяется как ожидаемый сценарий, а весь запуск pytest остаётся успешным.

Списком created пользуемся только для наблюдения за объектом, который иначе не был бы возвращён при ошибке. В реальном коде вместо него можно передать фабрику ресурса и записывать результат в тестовом двойнике. Главное — проверять состояние ресурса после исключения, а не только факт вызова функции очистки.

import pytest

class Session:
    def __init__(self):
        self.closed = False
    def close(self):
        self.closed = True

def build(valid, created):
    session = Session()
    created.append(session)
    try:
        if not valid:
            raise ValueError("invalid configuration")
        yield session
    finally:
        session.close()

@pytest.fixture
def session():
    yield from build(True, [])

def test_live_session(session):
    assert not session.closed

def test_cleanup_before_yield():
    created = []
    generator = build(False, created)
    with pytest.raises(ValueError, match="invalid configuration"):
        next(generator)
    assert created[0].closed

def test_cleanup_after_use():
    created = []
    generator = build(True, created)
    resource = next(generator)
    assert not resource.closed
    with pytest.raises(StopIteration):
        next(generator)
    assert resource.closed

Делать этапы подготовки короткими

Если фикстура приобретает несколько независимых ресурсов, разделите её на зависимые фикстуры или используйте ExitStack. Тогда успешный этап сразу получает свою очистку. Одна длинная процедура с заключительным блоком удаления затрудняет ответ на вопрос, какие объекты уже существуют в момент ошибки.

У finally также есть граница: он может освободить только успешно полученный ресурс. Если конструктор сам открыл дескриптор и затем выбросил исключение до возврата объекта, корректность очистки находится внутри конструктора или фабрики. Внешний код ещё не получил объект и вызвать его close не сможет.

Отработать жизненный цикл фикстуры

Откройте урок «Fixtures в pytest: подготовка данных и scope». Практика подготовки и областей видимости даёт основу для разбора очистки. Посмотрите программу и добавьте в свой проект проверку сбоя между созданием ресурса и передачей его тесту.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.