Spec-Zone.ru › pytest

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

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

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

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

# content of test_server.py

import pytest


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


@pytest.mark.device(serial="123")
def test_something_quick():
    pass


@pytest.mark.device(serial="abc")
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-9.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-9.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 ======================

Кроме того, вы можете ограничить запуск тестов, запуская только тесты, соответствующие одному или нескольким именованным аргументам маркера, например, чтобы запустить только тесты, помеченные device и конкретным serial="123":

$ pytest -v -m "device(serial='123')"
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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_something_quick PASSED                          [100%]

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

Примечание

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

Примечание

В выражениях маркеров поддерживаются только значения int, (неэкранированные) str, bool и None.

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

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

$ pytest -v test_server.py::TestClass::test_method
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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-9.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-9.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 --collect-only.

Использование -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-9.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-9.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» и «quick»:

$ pytest -k "http or quick" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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.toml
[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=strict_xfail): 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

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

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

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» — только ко второму тесту. Маркеры skip и xfail также можно применять таким способом; см. раздел Пропуск тестов и xfail с parametrize.

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

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

# 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-9.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-9.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=strict_xfail): 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-9.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 =======================

Обратите внимание: если указать платформу с помощью параметра командной строки для маркеров, например:

$ pytest -m linux
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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-9.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 ======================

или выбрать тесты «event» и «interface»:

$ pytest -m "interface or event" --tb=short
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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 ======================

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

Spec-Zone.ru

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