Сообщить об ошибке.

Область/scope действия фикстур модуля pytest в Python

Фикстуры, требующие доступа к сети, зависят от подключения и обычно требуют больших временных затрат на их создание. Следовательно, если фикстура используется несколькими тестами, то такое поведение может вызвать много накладных расходов. Чтобы избегать таких ситуаций фикстуры имеют аргумент scope - область действия/видимости.

Фикстуры создаются при первом запросе тестом и уничтожаются в зависимости от их области действия scope. Аргумент аргумент scope может принимать следующие параметры.

  • function: область действия по умолчанию, фикстура уничтожается в конце каждого теста, где она используется.
  • class: фикстура уничтожается во время завершения последнего теста в классе.
  • module: фикстура уничтожается во время завершения последнего теста в модуле.
  • package: фикстура уничтожается во время завершения последнего теста в пакете.
  • session: фикстура уничтожается в конце тестового сеанса.

Примечание: Фреймворк pytest кэширует только один экземпляр фикстуры за раз. Это означает, что при использовании фикстуры с параметрами pytest может вызывать фикстуру более одного раза в заданной области.

Содержание:


Пример области действия фикстуры.

Рассмотрим пример, где область действия фикстуры будет модуль, т.е. scope='module'.

В коде ниже в декоратор фикстуры @pytest.fixture добавлен аргумент scope="module" для того, чтобы функция smtp_connection() вызывалась только один раз для тестового модуля (по умолчанию параметр scope установлен в значение function). Таким образом, каждая тестовая функция модуля получит один и тот же экземпляр smtp_connection, что позволит сэкономить время на создание подключения.

import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as connect:
        yield connect

def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    # поднимем исключение, чтобы посмотреть 
    # что происходит в выводе `pytest`
    assert 0  

def test_noop(smtp_connection):
    response, _ = smtp_connection.noop()
    assert response == 250
    assert 0

Запустив этот тест $ pytest -v test.py видно, что оба оператора assert 0 выдали исключения, в результате чего можно увидеть значения входящих параметров. Анализируя вывод pytest делаем вывод, что в обе тестовые функции был передан один и тот же экземпляр smtp_connection. Следовательно, вместо двух, выполняется только одно SMTP-подключение для обеих тестовых функций.

Расширенный доступ к фикстуре для всех тестов.

Но как быть, если для другого тестового модуля нужно такое же подключение/фикстура? Конечно, для пакета можно установить scope='package' и перенести код фикстуры в __init__.py требуемого пакета. А как быть с сессией? Выход очень простой. Необходимо код фикстуры поместить в файл conftest.py (обычно создается пользователем в каталоге с модулями тестов), чтобы доступ к ней могли иметь разные тесты из разных модулей/пакетов тестового каталога:

# conftest.py
import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as connect:
        yield connect

И тесты в отдельном файле/модуле:

# test.py
def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    # поднимем исключение, чтобы посмотреть 
    # что происходит в выводе `pytest`
    assert 0  

def test_noop(smtp_connection):
    response, _ = smtp_connection.noop()
    assert response == 250
    assert 0

Если нужно иметь один экземпляр smtp_connection() на сеанс, то можно просто объявить его:

import pytest
import smtplib

@pytest.fixture(scope="session")
def smtp_connection():
    # возвращенное значение параметра 
    # будет общим для всех тестов
    ...

Соответственно, установив область действия scope="class", получим один экземпляр фикстуры для всех методов класса.

Изменяемая область действия фикстур.

В некоторых случаях может потребоваться динамически изменять область действия фикстур во время прохождения тестов. Для этого необходимо передать аргументу scope вызываемый объект (функцию), которая возвращает строку с требуемой областью действия и будет выполняться только один раз во время определения фикстуры. Этот вызываемый объект (функция) будет вызываться с двумя ключевыми аргументами: имя фикстуры fixture_name в виде строки и config с объектом конфигурации.

Такое поведение может быть полезно при работе с фикстурами, которым требуется какое то время для настройки, например, при создании контейнера docker. Например, можно использовать опцию командной строки для определения области действия процессов docker-контейнеров в разных виртуальных средах.

def determine_scope(fixture_name, config):
    """В зависимости от параметров CLI 
    возвращает строку для `scope`"""
    if config.getoption("--keep-containers", None):
        return "session"
    return "function"

# функция `determine_scope` передается в 
# качестве значения аргумента `scope`
@pytest.fixture(scope=determine_scope)
def docker_container():
    yield spawn_container()

Порядок создания фикстур.

При запросе функцией фикстуры сначала инициализиурются те из них, которые имеют более широкую областью действия. Другими словами, фикстуры, которые относятся к более глобальным областям (таким как session), выполняются раньше, чем к фикстурам с более узкой областью действия (таким как class или function).

В рамках одной тестовой функции порядок создания фикстур с одинаковой областью действия зависит от очередности вызова этих фикстур и установленных между ними связей.

import pytest

@pytest.fixture(scope="session")
def order():
    return []

@pytest.fixture
def func(order):
    order.append("function")

@pytest.fixture(scope="class")
def cls(order):
    order.append("class")

@pytest.fixture(scope="module")
def mod(order):
    order.append("module")

@pytest.fixture(scope="package")
def pack(order):
    order.append("package")

@pytest.fixture(scope="session")
def sess(order):
    order.append("session")

class TestClass:
    def test_order(self, func, cls, mod, pack, sess, order):
        assert order == ["session", "package", "module", "class", "function"]