Spec-Zone.ru › pytest

Написание функций-хуков

Проверка и выполнение функций-хуков

pytest вызывает функции-хуки из зарегистрированных плагинов для любой заданной спецификации хука. Рассмотрим типичную функцию-хук для хука pytest_collection_modifyitems(session, config, items), который pytest вызывает после завершения сбора всех тестовых элементов.

Когда мы реализуем в плагине функцию pytest_collection_modifyitems, pytest при регистрации проверит, что имена используемых вами аргументов соответствуют спецификации, и завершит работу с ошибкой, если это не так.

Рассмотрим возможную реализацию:

def pytest_collection_modifyitems(config, items):
    # called after collection is completed
    # you can modify the ``items`` list
    ...

Здесь pytest передаст config (объект конфигурации pytest) и items (список собранных тестовых элементов), но не передаст аргумент session, поскольку мы не указали его в сигнатуре функции. Такое динамическое «отсечение» аргументов позволяет pytest быть «совместимым с будущими версиями»: мы можем добавлять новые именованные параметры хуков, не нарушая сигнатуры существующих реализаций хуков. Это одна из причин общей долгосрочной совместимости плагинов pytest.

Обратите внимание: функциям-хукам, отличным от pytest_runtest_*, запрещено вызывать исключения. Это приведёт к сбою запуска pytest.

firstresult: остановка при первом результате, отличном от None

Большинство вызовов хуков pytest возвращают список результатов, содержащий все результаты вызванных функций-хуков, отличные от None.

В некоторых спецификациях хуков используется параметр firstresult=True, чтобы вызов хука продолжался лишь до тех пор, пока одна из N зарегистрированных функций не вернёт результат, отличный от None; этот результат и становится результатом всего вызова хука. В этом случае остальные функции-хуки вызваны не будут.

Обёртки хуков: выполнение вокруг других хуков

Плагины pytest могут реализовывать обёртки хуков, которые оборачивают выполнение других реализаций хуков. Обёртка хука — это функция-генератор, которая выполняет yield ровно один раз. Когда pytest вызывает хуки, сначала выполняются обёртки хуков, которым передаются те же аргументы, что и обычным хукам.

В точке yield обёртки хука pytest выполнит следующие реализации хуков и вернёт их результат в точку yield либо передаст исключение, если оно было вызвано.

Пример определения обёртки хука:

import pytest


@pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    do_something_before_next_hook_executes()

    # If the outcome is an exception, will raise the exception.
    res = yield

    new_res = post_process_result(res)

    # Override the return value to the plugin system.
    return new_res

Обёртка хука должна вернуть результат для хука или вызвать исключение.

Во многих случаях обёртке нужно лишь выполнять трассировку или другие побочные действия вокруг фактических реализаций хуков; тогда она может вернуть значение результата yield. Самая простая (хотя и бесполезная) обёртка хука — return (yield).

В других случаях обёртке нужно изменить или адаптировать результат; тогда она может вернуть новое значение. Если результат нижележащего хука — изменяемый объект, обёртка может изменить его, но, вероятно, этого лучше избегать.

Если реализация хука завершилась исключением, обёртка может обработать его с помощью try-catch-finally вокруг yield: передать его дальше, подавить или вызвать совершенно другое исключение.

Дополнительные сведения см. в документации pluggy об обёртках хуков.

Порядок функций-хуков / пример вызова

Для любой заданной спецификации хука может существовать несколько реализаций, поэтому мы обычно рассматриваем выполнение hook как вызов функции 1:N, где N — количество зарегистрированных функций. Есть способы влиять на то, будет ли реализация хука выполняться до или после других реализаций, то есть на её позицию в списке функций размером N:

# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
    # will execute as early as possible
    ...


# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    # will execute as late as possible
    ...


# Plugin 3
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items):
    # will execute even before the tryfirst one above!
    try:
        return (yield)
    finally:
        # will execute after all non-wrappers executed
        ...

Порядок выполнения:

  1. Вызывается pytest_collection_modifyitems из Plugin3, который выполняется до точки yield, поскольку это обёртка хука.
  2. Вызывается pytest_collection_modifyitems из Plugin1, поскольку он помечен как tryfirst=True.
  3. Вызывается pytest_collection_modifyitems из Plugin2, поскольку он помечен как trylast=True (но даже без этой пометки он выполнялся бы после Plugin1).
  4. Затем выполняется код pytest_collection_modifyitems из Plugin3 после точки yield. В yield возвращается результат вызова функций, не являющихся обёртками, либо вызывается исключение, если функции без обёрток вызвали исключение.

tryfirst и trylast можно также использовать для обёрток хуков; в этом случае они влияют на порядок выполнения обёрток хуков относительно друг друга.

Примечание

pytest ищет только реализации хуков, имена которых начинаются с pytest_. Аргумент specname функции @pytest.hookimpl можно использовать, чтобы задать реализации другой суффикс, например pytest_collection_modifyitems_tryfirst, однако имя функции всё равно должно начинаться с pytest_. Реализация хука с именем my_collection_modifyitems игнорируется, даже если она декорирована с помощью @pytest.hookimpl(specname="pytest_collection_modifyitems").

Объявление новых хуков

Примечание

Здесь кратко рассказывается о том, как добавлять новые хуки и как они работают в целом; более подробный обзор можно найти в документации pluggy.

Плагины и файлы conftest.py могут объявлять новые хуки, которые затем могут реализовывать другие плагины, чтобы изменить поведение или взаимодействовать с новым плагином:

pytest_addhooks(pluginmanager)

Вызывается при регистрации плагина, чтобы разрешить добавление новых хуков с помощью вызова pluginmanager.add_hookspecs(module_or_class, prefix).

Параметры:

pluginmanager — менеджер плагинов pytest.

Примечание

Этот хук несовместим с обёртками хуков.

Использование в плагинах conftest

Если плагин conftest реализует этот хук, он будет вызван сразу после регистрации conftest.

Обычно хуки объявляются как пустые функции, содержащие только документацию с описанием того, когда будет вызван хук и какие значения он должен возвращать. Имена функций должны начинаться с pytest_, иначе pytest их не распознает.

Пример. Предположим, этот код находится в модуле sample_hook.py.

def pytest_my_hook(config):
    """
    Receives the pytest config and does things with it
    """

Чтобы зарегистрировать хуки в pytest, их нужно поместить в отдельный модуль или класс. Затем этот класс или модуль можно передать в pluginmanager с помощью функции pytest_addhooks (которая сама является хуком, предоставляемым pytest).

def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'sample_hook' module."""
    from my_app.tests import sample_hook

    pluginmanager.add_hookspecs(sample_hook)

Пример из реального проекта см. в файле newhooks.py проекта xdist.

Хуки можно вызывать как из фикстур, так и из других хуков. В обоих случаях хуки вызываются через объект hook, доступный в объекте config. Большинство хуков напрямую получают объект config, а фикстуры могут использовать фикстуру pytestconfig, предоставляющую тот же объект.

@pytest.fixture()
def my_fixture(pytestconfig):
    # call the hook called "pytest_my_hook"
    # 'result' will be a list of return values from all registered functions.
    result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)

Примечание

Параметры передаются хукам только в виде именованных аргументов.

Теперь ваш хук готов к использованию. Чтобы зарегистрировать функцию для этого хука, другим плагинам или пользователям достаточно определить функцию pytest_my_hook с правильной сигнатурой в своём conftest.py.

Пример:

def pytest_my_hook(config):
    """
    Print all active hooks to the screen.
    """
    print(config.hook)

Примечание

В отличие от других хуков, хук pytest_generate_tests также обнаруживается, если он определён внутри тестового модуля или тестового класса. Остальные хуки должны находиться в плагинах conftest.py или внешних плагинах. См. раздел Параметризация фикстур и тестовых функций и раздел Хуки.

Использование хуков в pytest_addoption

Иногда необходимо изменить способ определения параметров командной строки одним плагином на основе хуков другого плагина. Например, плагин может предоставить параметр командной строки, для которого другому плагину нужно задать значение по умолчанию. Для этого можно использовать менеджер плагинов, чтобы установить хуки и вызвать их. Плагин объявит хуки, добавит их и будет использовать pytest_addoption следующим образом:

# contents of hooks.py


# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
def pytest_config_file_default_value():
    """Return the default value for the config file command line option."""


# contents of myplugin.py


def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'hooks' module."""
    from . import hooks

    pluginmanager.add_hookspecs(hooks)


def pytest_addoption(parser, pluginmanager):
    default_value = pluginmanager.hook.pytest_config_file_default_value()
    parser.addoption(
        "--config-file",
        help="Config file to use, defaults to %(default)s",
        default=default_value,
    )

Затем другой плагин (установленный через точки входа setuptools или параметр командной строки -p) может определить реализацию хука, задающую значение по умолчанию:

# contents of third_party_plugin.py


def pytest_config_file_default_value():
    return "config.yaml"

Примечание

Реализации хуков в файлах conftest.py недоступны другим плагинам во время их pytest_addoption() выполнения. Это связано с тем, что файлы conftest.py обнаруживаются и загружаются после инициализации встроенных плагинов, сторонних плагинов и плагинов командной строки (включая выполнение их хуков pytest_addoption()).

Однако начальные файлы conftest сами могут реализовывать pytest_addoption(), чтобы добавлять собственные параметры командной строки. При загрузке начального conftest его хук pytest_addoption() будет вызван немедленно.

Во время выполнения pytest_addoption() плагина будут доступны только реализации хуков из плагинов, загруженных ранее. К ним относятся:

  • встроенные плагины
  • плагины, явно загруженные с помощью -p в командной строке
  • установленные сторонние плагины (через точки входа setuptools)
  • плагины, указанные в переменной среды PYTEST_PLUGINS

Полный порядок обнаружения плагинов см. в разделе Порядок обнаружения плагинов при запуске инструмента.

Необязательное использование хуков сторонних плагинов

Использовать новые хуки из плагинов описанным выше способом может быть немного сложно из-за стандартного механизма проверки: если зависимый от вас плагин не установлен, проверка завершится ошибкой, а сообщение об ошибке будет малопонятно пользователям.

Можно отложить реализацию хука, создав для неё новый плагин вместо непосредственного объявления функций-хуков в модуле вашего плагина. Например:

# contents of myplugin.py


class DeferPlugin:
    """Simple plugin to defer pytest-xdist hook functions."""

    def pytest_testnodedown(self, node, error):
        """standard xdist hook function."""


def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

Это также позволяет устанавливать хуки условно, в зависимости от того, какие плагины установлены.

Хранение данных в элементах между вызовами функций-хуков

Плагинам часто требуется сохранить данные в Item в одной реализации хука, а затем получить к ним доступ в другой. Один из распространённых способов — напрямую присвоить элементу какой-либо приватный атрибут, однако средства проверки типов, например mypy, не одобряют такой подход, а кроме того, он может привести к конфликтам с другими плагинами. Поэтому pytest предлагает более удобный способ — item.stash.

Чтобы использовать «хранилище» в плагинах, сначала создайте «ключи хранилища» на верхнем уровне плагина:

been_there_key = pytest.StashKey[bool]()
done_that_key = pytest.StashKey[str]()

затем используйте ключи, чтобы сохранить данные в нужный момент:

def pytest_runtest_setup(item: pytest.Item) -> None:
    item.stash[been_there_key] = True
    item.stash[done_that_key] = "no"

и получите их в другом месте:

def pytest_runtest_teardown(item: pytest.Item) -> None:
    if not item.stash[been_there_key]:
        print("Oh?")
    item.stash[done_that_key] = "yes!"

Хранилища доступны для всех типов узлов (например, Class, Session), а также для Config, если это необходимо.

© 2015–2026 Holger Krekel and pytest-dev team
Licensed under the MIT License.
https://docs.pytest.org/en/stable/how-to/writing_hook_functions.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API