Работа с пользовательскими маркерами

Вот несколько примеров с использованием механизма Как пометить тестовые функции атрибутами.

Маркировка тестовых функций и выбор их для прогона

Вы можете «пометить» тестовую функцию пользовательскими метаданными следующим образом:

# content of test_server.py

import pytest


@pytest.mark.webtest
def test_send_http():
    pass  # perform some webtest test for your app


def test_something_quick():
    pass


def test_another():
    pass


class TestClass:
    def test_method(self):
        pass

Затем можно ограничить выполнение теста, чтобы запускать только тесты, помеченные webtest:

$ pytest -v -m webtest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selected

test_server.py::test_send_http PASSED                                [100%]

===================== 1 passed, 3 deselected in 0.12s ======================

Или наоборот, запустить все тесты, кроме webtest:

$ pytest -v -m "not webtest"
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 1 deselected / 3 selected

test_server.py::test_something_quick PASSED                          [ 33%]
test_server.py::test_another PASSED                                  [ 66%]
test_server.py::TestClass::test_method PASSED                        [100%]

===================== 3 passed, 1 deselected in 0.12s ======================

Выбор тестов на основе их идентификатора узла

Вы можете указать один или несколько node IDs в качестве позиционных аргументов, чтобы выбрать только указанные тесты. Это упрощает выборку тестов на основе имени их модуля, класса, метода или функции:

$ pytest -v test_server.py::TestClass::test_method
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 1 item

test_server.py::TestClass::test_method PASSED                        [100%]

============================ 1 passed in 0.12s =============================

Вы также можете выбрать класс:

$ pytest -v test_server.py::TestClass
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 1 item

test_server.py::TestClass::test_method PASSED                        [100%]

============================ 1 passed in 0.12s =============================

Или выберите несколько узлов:

$ pytest -v test_server.py::TestClass test_server.py::test_send_http
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 2 items

test_server.py::TestClass::test_method PASSED                        [ 50%]
test_server.py::test_send_http PASSED                                [100%]

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

Примечание

Идентификаторы узлов имеют вид module.py::class::method или module.py::function. Идентификаторы узлов контролируют, какие тесты будут собраны, поэтому module.py::class будет выбирать все методы тестирования класса. Узлы также создаются для каждого параметра параметризованного приспособления или теста, поэтому выбор параметризованного теста должен включать значение параметра, например, module.py::function[param].

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

Использование -k expr для выбора тестов на основе их названия

Добавлено в версии 2.0/2.3.4.

Вы можете использовать опцию командной строки -k, чтобы указать выражение, которое реализует подстрочное соответствие имен тестов вместо точного соответствия маркеров, которое обеспечивает -m. Это упрощает выборку тестов на основе их имен:

Изменено в версии 5.4.

Сопоставление выражений теперь не чувствительно к регистру.

$ pytest -v -k http  # running with the above defined example module
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selected

test_server.py::test_send_http PASSED                                [100%]

===================== 1 passed, 3 deselected in 0.12s ======================

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

$ pytest -k "not send_http" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 1 deselected / 3 selected

test_server.py::test_something_quick PASSED                          [ 33%]
test_server.py::test_another PASSED                                  [ 66%]
test_server.py::TestClass::test_method PASSED                        [100%]

===================== 3 passed, 1 deselected in 0.12s ======================

Или выбрать «http» и «быстрый» тесты:

$ pytest -k "http or quick" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 2 deselected / 2 selected

test_server.py::test_send_http PASSED                                [ 50%]
test_server.py::test_something_quick PASSED                          [100%]

===================== 2 passed, 2 deselected in 0.12s ======================

Вы можете использовать and, or, not и круглые скобки.

Помимо имени теста, -k также соответствует именам родителей теста (обычно это имя файла и класса, в котором он находится), атрибутам, установленным для функции теста, маркерам, примененным к нему или его родителям, и любым extra keywords, явно добавленным к нему или его родителям.

Регистрация маркеров

Регистрация маркеров для вашего набора тестов проста:

# content of pytest.ini
[pytest]
markers =
    webtest: mark a test as a webtest.
    slow: mark test as slow.

Можно зарегистрировать несколько пользовательских маркеров, определив каждый из них в отдельной строке, как показано в примере выше.

Вы можете спросить, какие маркеры существуют для вашего набора тестов - список включает только что определенные маркеры webtest и slow:

$ pytest --markers
@pytest.mark.webtest: mark a test as a webtest.

@pytest.mark.slow: mark test as slow.

@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings

@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.

@pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif

@pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail

@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info and examples.

@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures

@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.

@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.

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

Примечание

Рекомендуется явно регистрировать маркеры, чтобы:

  • В вашем тестовом наборе есть одно место, определяющее ваши маркеры

  • Запрос существующих маркеров через pytest --markers дает хороший результат

  • Опечатки в маркерах функций рассматриваются как ошибка, если вы используете опцию --strict-markers.

Отметка целых классов или модулей

Вы можете использовать декораторы pytest.mark с классами, чтобы применить маркеры ко всем его тестовым методам:

# content of test_mark_classlevel.py
import pytest


@pytest.mark.webtest
class TestClass:
    def test_startup(self):
        pass

    def test_startup_and_more(self):
        pass

Это эквивалентно прямому применению декоратора к двум тестовым функциям.

Чтобы применить метки на уровне модуля, используйте глобальную переменную pytestmark:

import pytest
pytestmark = pytest.mark.webtest

или несколько маркеров:

pytestmark = [pytest.mark.webtest, pytest.mark.slowtest]

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

import pytest


class TestClass:
    pytestmark = pytest.mark.webtest

Маркировка отдельных тестов при использовании параметризации

При использовании параметризации применение метки сделает ее применимой к каждому отдельному тесту. Однако можно также применить маркер к отдельному экземпляру теста:

import pytest


@pytest.mark.foo
@pytest.mark.parametrize(
    ("n", "expected"), [(1, 2), pytest.param(1, 3, marks=pytest.mark.bar), (2, 3)]
)
def test_increment(n, expected):
    assert n + 1 == expected

В этом примере метка «foo» будет применена к каждому из трех тестов, тогда как метка «bar» будет применена только ко второму тесту. Таким же образом можно применять метки «пропуск» и «xfail», см. раздел Пропуск/xfail с параметризацией.

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

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

# content of conftest.py

import pytest


def pytest_addoption(parser):
    parser.addoption(
        "-E",
        action="store",
        metavar="NAME",
        help="only run tests matching the environment NAME.",
    )


def pytest_configure(config):
    # register an additional marker
    config.addinivalue_line(
        "markers", "env(name): mark test to run only on named environment"
    )


def pytest_runtest_setup(item):
    envnames = [mark.args[0] for mark in item.iter_markers(name="env")]
    if envnames:
        if item.config.getoption("-E") not in envnames:
            pytest.skip(f"test requires env in {envnames!r}")

Тестовый файл, использующий этот локальный плагин:

# content of test_someenv.py

import pytest


@pytest.mark.env("stage1")
def test_basic_db_operation():
    pass

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

$ pytest -E stage2
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_someenv.py s                                                    [100%]

============================ 1 skipped in 0.12s ============================

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

$ pytest -E stage1
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_someenv.py .                                                    [100%]

============================ 1 passed in 0.12s =============================

Опция --markers всегда выдает список доступных маркеров:

$ pytest --markers
@pytest.mark.env(name): mark test to run only on named environment

@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings

@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.

@pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif

@pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail

@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info and examples.

@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures

@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.

@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.

Передача вызываемого объекта пользовательским маркерам

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

# content of conftest.py
import sys


def pytest_runtest_setup(item):
    for marker in item.iter_markers(name="my_marker"):
        print(marker)
        sys.stdout.flush()

Пользовательский маркер может иметь свой набор аргументов, то есть свойства args и kwargs, определяемые либо вызовом его как вызываемого, либо с помощью pytest.mark.MARKER_NAME.with_args. В большинстве случаев эти два метода достигают одного и того же эффекта.

Однако, если в качестве единственного позиционного аргумента имеется вызываемый объект без аргументов в виде ключевых слов, использование pytest.mark.MARKER_NAME(c) не передаст c в качестве позиционного аргумента, а украсит c пользовательским маркером (см. MarkDecorator). К счастью, на помощь приходит pytest.mark.MARKER_NAME.with_args:

# content of test_custom_marker.py
import pytest


def hello_world(*args, **kwargs):
    return "Hello World"


@pytest.mark.my_marker.with_args(hello_world)
def test_with_args():
    pass

На выходе получаем следующее:

$ pytest -q -s
Mark(name='my_marker', args=(<function hello_world at 0xdeadbeef0001>,), kwargs={})
.
1 passed in 0.12s

Мы видим, что у пользовательского маркера набор аргументов расширен функцией hello_world. Это ключевое различие между созданием пользовательского маркера как вызываемой функции, которая вызывает __call__ за кулисами, и использованием with_args.

Чтение маркеров, которые были установлены из нескольких мест

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

# content of test_mark_three_times.py
import pytest

pytestmark = pytest.mark.glob("module", x=1)


@pytest.mark.glob("class", x=2)
class TestClass:
    @pytest.mark.glob("function", x=3)
    def test_something(self):
        pass

Здесь мы имеем маркер «glob», примененный три раза к одной и той же тестовой функции. Из файла conftest мы можем прочитать это следующим образом:

# content of conftest.py
import sys


def pytest_runtest_setup(item):
    for mark in item.iter_markers(name="glob"):
        print(f"glob args={mark.args} kwargs={mark.kwargs}")
        sys.stdout.flush()

Давайте запустим его без захвата вывода и посмотрим, что мы получим:

$ pytest -q -s
glob args=('function',) kwargs={'x': 3}
glob args=('class',) kwargs={'x': 2}
glob args=('module',) kwargs={'x': 1}
.
1 passed in 0.12s

Разметка тестов для конкретной платформы с помощью pytest

Предположим, у вас есть набор тестов, в котором тесты помечены для определенных платформ, а именно pytest.mark.darwin, pytest.mark.win32 и т.д., и у вас также есть тесты, которые выполняются на всех платформах и не имеют конкретного маркера. Если вы хотите иметь возможность запускать только тесты для вашей конкретной платформы, вы можете использовать следующий плагин:

# content of conftest.py
#
import sys

import pytest

ALL = set("darwin linux win32".split())


def pytest_runtest_setup(item):
    supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
    plat = sys.platform
    if supported_platforms and plat not in supported_platforms:
        pytest.skip(f"cannot run on platform {plat}")

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

# content of test_plat.py

import pytest


@pytest.mark.darwin
def test_if_apple_is_evil():
    pass


@pytest.mark.linux
def test_if_linux_works():
    pass


@pytest.mark.win32
def test_if_win32_crashes():
    pass


def test_runs_everywhere():
    pass

то вы увидите два пропущенных теста и два выполненных, как и ожидалось:

$ pytest -rs # this option reports skip reasons
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items

test_plat.py s.s.                                                    [100%]

========================= short test summary info ==========================
SKIPPED [2] conftest.py:13: cannot run on platform linux
======================= 2 passed, 2 skipped in 0.12s =======================

Обратите внимание, что если вы укажете платформу через опцию командной строки marker-command line следующим образом:

$ pytest -m linux
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 3 deselected / 1 selected

test_plat.py .                                                       [100%]

===================== 1 passed, 3 deselected in 0.12s ======================

то немаркированные тесты выполняться не будут. Таким образом, это способ ограничить выполнение только определенных тестов.

Автоматическое добавление маркеров на основе названий тестов

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

# content of test_module.py


def test_interface_simple():
    assert 0


def test_interface_complex():
    assert 0


def test_event_simple():
    assert 0


def test_something_else():
    assert 0

Мы хотим динамически определить два маркера и можем сделать это в плагине conftest.py:

# content of conftest.py

import pytest


def pytest_collection_modifyitems(items):
    for item in items:
        if "interface" in item.nodeid:
            item.add_marker(pytest.mark.interface)
        elif "event" in item.nodeid:
            item.add_marker(pytest.mark.event)

Теперь мы можем использовать -m option для выбора одного набора:

$ pytest -m interface --tb=short
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 2 deselected / 2 selected

test_module.py FF                                                    [100%]

================================= FAILURES =================================
__________________________ test_interface_simple ___________________________
test_module.py:4: in test_interface_simple
    assert 0
E   assert 0
__________________________ test_interface_complex __________________________
test_module.py:8: in test_interface_complex
    assert 0
E   assert 0
========================= short test summary info ==========================
FAILED test_module.py::test_interface_simple - assert 0
FAILED test_module.py::test_interface_complex - assert 0
===================== 2 failed, 2 deselected in 0.12s ======================

или выбрать оба теста «событие» и «интерфейс»:

$ pytest -m "interface or event" --tb=short
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 1 deselected / 3 selected

test_module.py FFF                                                   [100%]

================================= FAILURES =================================
__________________________ test_interface_simple ___________________________
test_module.py:4: in test_interface_simple
    assert 0
E   assert 0
__________________________ test_interface_complex __________________________
test_module.py:8: in test_interface_complex
    assert 0
E   assert 0
____________________________ test_event_simple _____________________________
test_module.py:12: in test_event_simple
    assert 0
E   assert 0
========================= short test summary info ==========================
FAILED test_module.py::test_interface_simple - assert 0
FAILED test_module.py::test_interface_complex - assert 0
FAILED test_module.py::test_event_simple - assert 0
===================== 3 failed, 1 deselected in 0.12s ======================
Вернуться на верх