Arbeiten mit benutzerdefinierten Markern

Hier sind einige Beispiele, die den Mechanismus zur Kennzeichnung von Testfunktionen mit Attributen verwenden.

Testfunktionen markieren und für einen Lauf auswählen

Sie können eine Testfunktion mit benutzerdefinierten Metadaten wie folgt „markieren“

# 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

Sie können dann einen Testlauf auf die Ausführung von Tests beschränken, die mit webtest markiert sind

$ 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 ======================

Oder umgekehrt, alle Tests außer den webtest-Tests ausführen

$ 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 ======================

Zusätzlich können Sie einen Testlauf auf die Ausführung von Tests beschränken, die mit einem oder mehreren Marker-Schlüsselwortargumenten übereinstimmen, z. B. um nur Tests auszuführen, die mit device und dem spezifischen serial="123" markiert sind

$ 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 ======================

Hinweis

In Marker-Ausdrücken wird nur die Übereinstimmung von Schlüsselwortargumenten unterstützt.

Hinweis

Nur int, (unverpackte) str, bool & None Werte werden in Marker-Ausdrücken unterstützt.

Tests basierend auf ihrer Node-ID auswählen

Sie können eine oder mehrere Node-IDs als Positionsargumente übergeben, um nur bestimmte Tests auszuwählen. Dies erleichtert die Auswahl von Tests basierend auf ihrem Modul-, Klassen-, Methoden- oder Funktionsnamen.

$ 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 =============================

Sie können auch auf der Klasse auswählen

$ 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 =============================

Oder mehrere Knoten auswählen

$ 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 =============================

Hinweis

Node-IDs sind im Format modul.py::klasse::methode oder modul.py::funktion. Node-IDs steuern, welche Tests gesammelt werden, sodass modul.py::klasse alle Testmethoden der Klasse auswählt. Knoten werden auch für jeden Parameter einer parametrisierten Fixture oder eines Tests erstellt. Daher muss die Auswahl eines parametrisierten Tests den Parameterwert enthalten, z. B. modul.py::funktion[param].

Node-IDs für fehlerhafte Tests werden in den Testzusammenfassungsinformationen angezeigt, wenn pytest mit der Option -rf ausgeführt wird. Sie können Node-IDs auch aus der Ausgabe von pytest --collect-only erstellen.

Verwendung von -k expr zur Auswahl von Tests basierend auf ihrem Namen

Hinzugefügt in Version 2.0/2.3.4.

Sie können die Befehlszeilenoption -k verwenden, um einen Ausdruck anzugeben, der eine Teilstring-Übereinstimmung mit den Testnamen implementiert, anstatt der exakten Übereinstimmung mit Markern, die -m bietet. Dies erleichtert die Auswahl von Tests basierend auf ihren Namen.

Geändert in Version 5.4.

Die Ausdrucksabstimmung ist jetzt case-insensitiv.

$ 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 ======================

Und Sie können auch alle Tests ausführen, außer denen, die mit dem Schlüsselwort übereinstimmen

$ 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 ======================

Oder um „http“- und „quick“-Tests auszuwählen

$ 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 ======================

Sie können and, or, not und Klammern verwenden.

Zusätzlich zum Namen des Tests gleicht -k auch die Namen der Eltern des Tests (normalerweise der Name der Datei und der Klasse, in der er sich befindet), Attribute, die auf der Testfunktion gesetzt sind, Marker, die auf ihn oder seine Eltern angewendet wurden, und alle zusätzlichen Schlüsselwörter ab, die ihm oder seinen Eltern explizit hinzugefügt wurden.

Marker registrieren

Das Registrieren von Markern für Ihre Testsuite ist einfach

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

Mehrere benutzerdefinierte Marker können registriert werden, indem jeder in einer eigenen Zeile definiert wird, wie im obigen Beispiel gezeigt.

Sie können fragen, welche Marker für Ihre Testsuite existieren - die Liste enthält unsere gerade definierten Marker webtest und 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.de/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.de/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.de/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.de/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.de/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.

Ein Beispiel, wie man Marker von einem Plugin hinzufügt und damit arbeitet, finden Sie unter Benutzerdefinierter Marker und Befehlszeilenoption zur Steuerung von Testläufen.

Hinweis

Es wird empfohlen, Marker explizit zu registrieren, damit

  • Es gibt einen Ort in Ihrer Testsuite, an dem Ihre Marker definiert sind.

  • Das Abfragen bestehender Marker über pytest --markers liefert gute Ausgaben.

  • Tippfehler bei Funktionsmarkern werden als Fehler behandelt, wenn Sie die Konfigurationsoption strict_markers verwenden.

Ganze Klassen oder Module markieren

Sie können pytest.mark-Dekoratoren mit Klassen verwenden, um Marker auf alle Testmethoden anzuwenden.

# 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

Dies entspricht dem direkten Anwenden des Dekorators auf die beiden Testfunktionen.

Um Markierungen auf Modulebene anzuwenden, verwenden Sie die globale Variable pytestmark.

import pytest
pytestmark = pytest.mark.webtest

oder mehrere Marker

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

Aus historischen Gründen, bevor Klassen-Dekoratoren eingeführt wurden, ist es möglich, das Attribut pytestmark wie folgt auf eine Testklasse zu setzen.

import pytest


class TestClass:
    pytestmark = pytest.mark.webtest

Individuelle Tests beim Parametrisieren markieren

Bei Verwendung von Parametrisierung wird eine Markierung auf jeden einzelnen Test angewendet. Es ist jedoch auch möglich, eine Markierung auf eine einzelne Testinstanz anzuwenden.

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

In diesem Beispiel wird die Markierung „foo“ auf jeden der drei Tests angewendet, während die Markierung „bar“ nur auf den zweiten Test angewendet wird. Skip- und xfail-Markierungen können auch auf diese Weise angewendet werden, siehe Skip/xfail mit Parametrisierung.

Benutzerdefinierter Marker und Befehlszeilenoption zur Steuerung von Testläufen

Plugins können benutzerdefinierte Marker bereitstellen und basierend darauf spezifisches Verhalten implementieren. Dies ist ein in sich geschlossenes Beispiel, das eine Befehlszeilenoption und einen parametrisierten Testfunktionsmarker hinzufügt, um Tests auszuführen, die über benannte Umgebungen angegeben wurden.

# 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}")

Eine Testdatei, die dieses lokale Plugin verwendet.

# content of test_someenv.py

import pytest


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

und eine Beispielaufrufe, die eine andere Umgebung als die benötigte angeben.

$ 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 ============================

und hier ist eine, die genau die benötigte Umgebung angibt.

$ 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 =============================

Die Option --markers liefert immer eine Liste der verfügbaren Marker.

$ 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.de/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.de/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.de/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.de/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.de/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.

Übergabe eines Aufrufbaren an benutzerdefinierte Marker

Unten sehen Sie die Konfigurationsdatei, die in den nächsten Beispielen verwendet wird.

# 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()

Ein benutzerdefinierter Marker kann seine Argumente haben, d. h. args und kwargs, die entweder durch Aufruf als Aufrufbarer oder durch Verwendung von pytest.mark.MARKER_NAME.with_args definiert werden. Diese beiden Methoden erzielen meist denselben Effekt.

Wenn jedoch ein Aufrufbarer als einziges Positionsargument ohne Schlüsselwortargumente vorhanden ist, wird die Verwendung von pytest.mark.MARKER_NAME(c) c nicht als Positionsargument übergeben, sondern c mit dem benutzerdefinierten Marker dekorieren (siehe MarkDecorator). Glücklicherweise hilft 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

Die Ausgabe ist wie folgt:

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

Wir sehen, dass das Argument des benutzerdefinierten Markers mit der Funktion hello_world erweitert wurde. Dies ist der Hauptunterschied zwischen der Erstellung eines benutzerdefinierten Markers als Aufrufbarer, der im Hintergrund __call__ aufruft, und der Verwendung von with_args.

Marker aus mehreren Stellen lesen

Wenn Sie Marker in Ihrer Testsuite intensiv nutzen, können Sie auf Fälle stoßen, in denen ein Marker mehrmals auf eine Testfunktion angewendet wird. Aus dem Plugin-Code können Sie alle solchen Einstellungen lesen. Beispiel:

# 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

Hier haben wir den Marker „glob“ dreimal auf dieselbe Testfunktion angewendet. Aus einer conftest-Datei können wir ihn wie folgt lesen:

# 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()

Lassen Sie uns dies ohne Erfassung der Ausgabe ausführen und sehen, was wir erhalten.

$ 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

Plattformspezifische Tests mit pytest markieren

Betrachten Sie eine Testsuite, die Tests für bestimmte Plattformen markiert, nämlich pytest.mark.darwin, pytest.mark.win32 usw., und Sie haben auch Tests, die auf allen Plattformen laufen und keine spezifische Markierung haben. Wenn Sie nun eine Möglichkeit haben möchten, nur die Tests für Ihre spezifische Plattform auszuführen, könnten Sie das folgende Plugin verwenden:

# 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}")

dann werden Tests übersprungen, wenn sie für eine andere Plattform angegeben wurden. Machen wir eine kleine Testdatei, um zu zeigen, wie das aussieht:

# 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

dann werden wie erwartet zwei Tests übersprungen und zwei Tests ausgeführt.

$ 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 =======================

Beachten Sie, dass wenn Sie eine Plattform über die Marker-Befehlszeilenoption wie folgt angeben:

$ 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 ======================

dann werden die unmarked-Tests nicht ausgeführt. Es ist somit eine Möglichkeit, die Ausführung auf die spezifischen Tests zu beschränken.

Marker automatisch basierend auf Testnamen hinzufügen

Wenn Sie eine Testsuite haben, in der Testfunktionsnamen einen bestimmten Testtyp angeben, können Sie einen Hook implementieren, der automatisch Marker definiert, damit Sie die Option -m damit verwenden können. Betrachten wir dieses Testmodul:

# 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

Wir möchten dynamisch zwei Marker definieren und können dies in einem conftest.py-Plugin tun.

# 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)

Wir können jetzt die Option -m option verwenden, um eine Gruppe auszuwählen.

$ 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 ======================

oder um sowohl „event“- als auch „interface“-Tests auszuwählen.

$ 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 ======================