Написание плагинов

Легко реализовать local conftest plugins для собственного проекта или pip-installable plugins, который можно использовать во многих проектах, включая проекты сторонних разработчиков. Пожалуйста, обратитесь к Как устанавливать и использовать плагины, если вы хотите только использовать, но не писать плагины.

Плагин содержит одну или несколько хук-функций. Writing hooks объясняет основы и детали того, как вы можете самостоятельно написать хук-функцию. pytest реализует все аспекты конфигурации, сбора, запуска и отчетности путем вызова well specified hooks следующих плагинов:

  • встроенные плагины: загружаются из внутреннего каталога pytest _pytest.

  • external plugins: модули, обнаруженные через setuptools entry points

  • conftest.py plugins: автоматическое обнаружение модулей в тестовых каталогах

В принципе, каждый вызов хука - это вызов функции Python 1:N, где N - количество зарегистрированных функций реализации для данной спецификации. Все спецификации и реализации следуют соглашению об именовании с префиксом pytest_, что позволяет легко их различать и находить.

Порядок открытия плагина при запуске инструмента

pytest загружает подключаемые модули при запуске инструмента следующим образом:

  1. сканируя командную строку на наличие опции -p no:name и блокируя этот плагин от загрузки (даже встроенные плагины могут быть заблокированы таким образом). Это происходит до обычного разбора командной строки.

  2. загрузив все встроенные плагины.

  3. сканируя командную строку на наличие опции -p name и загружая указанный плагин. Это происходит до обычного разбора командной строки.

  4. путем загрузки всех плагинов, зарегистрированных через setuptools entry points.

  5. загружая все плагины, указанные через переменную окружения PYTEST_PLUGINS.

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

    • если пути тестирования не указаны, используйте текущий каталог в качестве пути тестирования

    • если существует, загрузите conftest.py и test*/conftest.py относительно части каталога первого тестового пути. После загрузки файла conftest.py загрузите все плагины, указанные в его переменной pytest_plugins, если она существует.

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

  7. рекурсивно загружая все плагины, указанные переменной pytest_plugins в файлах conftest.py.

conftest.py: локальные плагины для каждого каталога

Локальные плагины conftest.py содержат специфические для каталога реализации хуков. Сессия хука и выполнение тестов вызовут все хуки, определенные в файлах conftest.py, расположенных ближе к корню файловой системы. Пример реализации хука pytest_runtest_setup так, чтобы он вызывался для тестов в подкаталоге a, но не для других каталогов:

a/conftest.py:
    def pytest_runtest_setup(item):
        # called for running each test in 'a' directory
        print("setting up", item)

a/test_sub.py:
    def test_sub():
        pass

test_flat.py:
    def test_flat():
        pass

Вот как его можно запустить:

pytest test_flat.py --capture=no  # will not show "setting up"
pytest a/test_sub.py --capture=no  # will show "setting up"

Примечание

Если у вас есть файлы conftest.py, которые не находятся в каталоге пакета python (т.е. в каталоге, содержащем __init__.py), то «import conftest» может быть неоднозначным, поскольку в вашем каталоге conftest.py или PYTHONPATH могут быть и другие файлы sys.path. Поэтому хорошей практикой для проектов является либо помещать conftest.py в область видимости пакета, либо никогда не импортировать ничего из файла conftest.py.

См. также: механизмы импорта pytest и sys.path/PYTHONPATH.

Примечание

Некоторые хуки должны быть реализованы только в плагинах или файлах conftest.py, расположенных в корневом каталоге тестов, из-за того, как pytest обнаруживает плагины при запуске, подробности смотрите в документации к каждому хуку.

Написание собственного плагина

Если вы хотите написать плагин, существует множество примеров из реальной жизни, которые вы можете скопировать:

Все эти плагины используют hooks и/или fixtures для расширения и добавления функциональности.

Примечание

Обязательно ознакомьтесь с замечательным проектом cookiecutter-pytest-plugin, который представляет собой cookiecutter template для создания плагинов.

Шаблон представляет собой отличную отправную точку с работающим плагином, тестами, запущенными с помощью tox, полным файлом README, а также предварительно сконфигурированной точкой входа.

Также рассмотрите вариант contributing your plugin to pytest-dev, когда у него появится несколько счастливых пользователей, кроме вас.

Сделать ваш плагин доступным для установки другим пользователям

Если вы хотите сделать свой плагин внешне доступным, вы можете определить так называемую точку входа для вашего дистрибутива, чтобы pytest находил ваш модуль плагина. Точки входа - это возможность, предоставляемая setuptools.

pytest ищет точку входа pytest11 для обнаружения своих плагинов, поэтому вы можете сделать свой плагин доступным, определив его в своем файле pyproject.toml.

# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "myproject"
classifiers = [
    "Framework :: Pytest",
]

[tool.setuptools]
packages = ["myproject"]

[project.entry_points]
pytest11 = [
    "myproject = myproject.pluginmodule",
]

Если пакет установлен таким образом, pytest загрузит myproject.pluginmodule как плагин, который может определить hooks. Подтвердите регистрацию с помощью pytest --trace-config

Примечание

Обязательно включите Framework :: Pytest в список PyPI classifiers, чтобы пользователям было легко найти ваш плагин.

Переписывание утверждений

Одной из главных особенностей pytest является использование простых утверждений assert и детальная интроспекция выражений при ошибках утверждений. Это обеспечивается «переписыванием утверждений», которое модифицирует разобранный AST до того, как он будет скомпилирован в байткод. Это делается с помощью крючка импорта PEP 302, который устанавливается на ранней стадии запуска pytest и будет выполнять эту перезапись при импорте модулей. Однако, поскольку мы не хотим тестировать байткод, отличный от того, который вы будете использовать в производстве, этот хук переписывает только сами тестовые модули (как определено опцией конфигурации python_files), и любые модули, которые являются частью плагинов. Любой другой импортированный модуль не будет переписан, и будет происходить обычное поведение утверждений.

Если у вас есть помощники утверждений в других модулях, где вам потребуется включить перезапись утверждений, вам нужно явно попросить pytest переписать этот модуль до того, как он будет импортирован.

register_assert_rewrite(*names)[исходный код]

Зарегистрируйте одно или несколько имен модулей, которые будут переписаны при импорте.

Эта функция гарантирует, что этот модуль или все модули внутри пакета получат свои утверждения assert переписанными. Таким образом, вы должны убедиться, что эта функция будет вызвана до того, как модуль будет импортирован, обычно в вашем __init__.py, если вы являетесь плагином, использующим пакет.

Parameters:

names (str) – Имена модулей для регистрации.

Это особенно важно, когда вы пишете подключаемый модуль pytest, который создается с помощью пакета. Хук импорта рассматривает как подключаемые модули только файлы conftest.py и любые модули, перечисленные в точке входа pytest11. В качестве примера рассмотрим следующий пакет:

pytest_foo/__init__.py
pytest_foo/plugin.py
pytest_foo/helper.py

Со следующим типичным setup.py экстрактом:

setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)

В этом случае будет переписано только pytest_foo/plugin.py. Если вспомогательный модуль также содержит утверждения, которые должны быть переписаны, его необходимо пометить как таковой, прежде чем он будет импортирован. Это проще всего сделать, пометив его для перезаписи внутри модуля __init__.py, который всегда импортируется первым, когда импортируется модуль внутри пакета. Таким образом, plugin.py все еще может нормально импортировать helper.py. Содержимое pytest_foo/__init__.py должно выглядеть следующим образом:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

Требование/загрузка плагинов в тестовый модуль или файл conftest

Вы можете потребовать плагины в тестовом модуле или в файле conftest.py с помощью pytest_plugins:

pytest_plugins = ["name1", "name2"]

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

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins обрабатываются рекурсивно, поэтому обратите внимание, что в примере выше, если myapp.testsupport.myplugin также объявляет pytest_plugins, содержимое переменной также будет загружено как плагины, и так далее.

Примечание

Требование использования плагинами переменной pytest_plugins в некорневых conftest.py файлах устарело.

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

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

Плагины, импортированные с помощью pytest_plugins, также будут автоматически помечены для перезаписи утверждений (см. pytest.register_assert_rewrite()). Однако для того, чтобы это имело эффект, модуль не должен быть уже импортирован; если он уже был импортирован в момент обработки оператора pytest_plugins, будет выдано предупреждение, и утверждения внутри плагина не будут переписаны. Чтобы исправить это, вы можете либо сами вызвать pytest.register_assert_rewrite() до того, как модуль будет импортирован, либо расположить код так, чтобы отложить импортирование до момента регистрации плагина.

Доступ к другому плагину по имени

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

plugin = config.pluginmanager.get_plugin("name_of_plugin")

Если вы хотите посмотреть имена существующих плагинов, используйте опцию --trace-config.

Регистрация пользовательских маркеров

Если ваш плагин использует какие-либо маркеры, вы должны зарегистрировать их так, чтобы они появлялись в тексте справки pytest и не cause spurious warnings. Например, следующий плагин зарегистрирует cool_marker и mark_with для всех пользователей:

def pytest_configure(config):
    config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
    config.addinivalue_line(
        "markers", "mark_with(arg, arg2): this marker takes arguments."
    )

Тестирование плагинов

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

Вы можете сделать это, добавив следующую строку в файл conftest.py в вашем каталоге тестирования:

# content of conftest.py

pytest_plugins = ["pytester"]

Также вы можете вызвать pytest с помощью опции командной строки -p pytester.

Это позволит вам использовать приспособление pytester для тестирования кода вашего плагина.

Давайте продемонстрируем, что можно сделать с помощью плагина на примере. Представьте, что мы разработали плагин, который предоставляет приспособление hello, которое выдает функцию, и мы можем вызвать эту функцию с одним необязательным параметром. Она вернет строковое значение Hello World!, если мы не предоставим значение, или Hello {value}!, если мы предоставим строковое значение.

import pytest


def pytest_addoption(parser):
    group = parser.getgroup("helloworld")
    group.addoption(
        "--name",
        action="store",
        dest="name",
        default="World",
        help='Default "name" for hello().',
    )


@pytest.fixture
def hello(request):
    name = request.config.getoption("name")

    def _hello(name=None):
        if not name:
            name = request.config.getoption("name")
        return f"Hello {name}!"

    return _hello

Теперь приспособление pytester предоставляет удобный API для создания временных файлов conftest.py и файлов тестов. Оно также позволяет нам запускать тесты и возвращать объект результата, с помощью которого мы можем утверждать результаты тестов.

def test_hello(pytester):
    """Make sure that our plugin works."""

    # create a temporary conftest.py file
    pytester.makeconftest(
        """
        import pytest

        @pytest.fixture(params=[
            "Brianna",
            "Andreas",
            "Floris",
        ])
        def name(request):
            return request.param
    """
    )

    # create a temporary pytest test file
    pytester.makepyfile(
        """
        def test_hello_default(hello):
            assert hello() == "Hello World!"

        def test_hello_name(hello, name):
            assert hello(name) == "Hello {0}!".format(name)
    """
    )

    # run all tests with pytest
    result = pytester.runpytest()

    # check that all 4 tests passed
    result.assert_outcomes(passed=4)

Дополнительно можно скопировать примеры в изолированное окружение pytester перед запуском pytest на нем. Таким образом, мы можем абстрагировать тестируемую логику в отдельные файлы, что особенно полезно для длинных тестов и/или длинных conftest.py файлов.

Обратите внимание, что для работы pytester.copy_example нам нужно установить pytester_example_dir в нашем pytest.ini, чтобы указать pytest, где искать файлы примеров.

# content of pytest.ini
[pytest]
pytester_example_dir = .
# content of test_example.py


def test_plugin(pytester):
    pytester.copy_example("test_example.py")
    pytester.runpytest("-k", "test_example")


def test_example():
    pass
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
collected 2 items

test_example.py ..                                                   [100%]

============================ 2 passed in 0.12s =============================

Для получения дополнительной информации об объекте результата, который возвращает runpytest(), и методах, которые он предоставляет, пожалуйста, ознакомьтесь с документацией RunResult.

Вернуться на верх