Markers ======= Marker in pytest kann man sich wie Tags oder Etiketten vorstellen. Wenn einige Tests langsam sind, könnt ihr sie mit ``@pytest.mark.slow`` markieren und pytest diese Tests überspringen lassen, wenn ihr in Eile seid. Ihr könnt eine Handvoll Tests aus einer Testsuite auswählen und sie mit ``@pytest.mark.smoke`` markieren und diese als erste Stufe einer Testpipeline in einem :term:`CI`-System ausführen. Ihr könnt wirklich für jeden Grund, den ihr habt, um nur einige Tests auszuführen, Marker verwenden. pytest enthält eine Handvoll Built-in-Marker, die das Verhalten der Testausführung verändern. Eine davon, ``@pytest.mark.parametrize``, haben wir bereits in :ref:`parameterise-functions` verwendet. Zusätzlich zu den benutzerdefinierten Markierungen, die wir erstellen und zu unseren Tests hinzufügen können, weisen die Built-in-Marker pytest an, etwas Besonderes mit den markierten Tests zu tun. Im Folgenden werden wir beide Arten von Markern genauer untersuchen: die Built-in-Marker, die das Verhalten ändern, und die benutzerdefinierten Marker, die wir erstellen können, um auszuwählen, welche Tests ausgeführt werden sollen. Wir können Marker auch verwenden, um Informationen an eine Fixture zu übergeben, die von einem Test verwendet wird. Built-in-Markers verwenden -------------------------- Die Built-in-Markers von pytest werden verwendet, um die Testausführung zu verändern. Hier ist die vollständige Liste der Built-in-Markers, die in pytest enthalten sind: :samp:`@pytest.mark.filterwarnings({WARNUNG})` Dieser Marker fügt dem angegebenen Test einen Warnfilter hinzu. :samp:`@pytest.mark.skip(reason={None})` Mit diesem Marker wird der Test mit einem optionalen Grund übersprungen. :samp:`@pytest.mark.skipif({BEDINGUNG}, ...*, {GRUND})` Diese Markierung überspringt den Test, wenn eine der Bedingungen True ist. :samp:`@pytest.mark.xfail({BEDINGUNG}, ...* {GRUND}, run={True}, raises={None}, strict={strict_xfail})` Dieser Marker teilt pytest mit, dass wir das Fehlschlagen erwarten. :samp:`@pytest.mark.parametrize({ARG1, ARG2, ...` Dieser Marker ruft eine Testfunktion mehrfach auf, wobei nacheinander verschiedene Argumente übergeben werden. :samp:`@pytest.mark.usefixtures({FIXTURE1, FIXTURE2, ...` Dieser Marker kennzeichnet Tests, die alle angegebenen Fixtures benötigen. :doc:`@pytest.mark.parametrize ` haben wir bereits verwendet. Lasst uns die drei anderen, am häufigsten verwendeten Built-in-Markers mit einigen Beispielen durchgehen, um zu sehen, wie sie funktionieren. Überspringen von Tests mit ``@pytest.mark.skip`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Der Marker ``skip`` erlaubt es uns, einen Test zu überspringen. Nehmen wir an, wir wollen in einer zukünftigen Version der Tasks-Anwendung die Möglichkeit zum Sortieren hinzufügen und möchten, dass die :class:`Task`-Klasse Vergleiche unterstützt. Wir schreiben einen Test für den Vergleich von :class:`Task`-Objekten mit ``<`` wie folgt: .. code-block:: python from cusy.tasks import Task def test_less_than(): i1 = Task("Update pytest section") i2 = Task("Update cibuildwheel section") assert i1 < i2 def test_equality(): i1 = Task("Update pytest section") i2 = Task("Update pytest section") assert i1 == i2 Und er scheitert: .. code-block:: pytest $ uv run pytest --tb=short tests/test_compare.py ============================= test session starts ============================== … collected 2 items tests/test_compare.py F. [100%] =================================== FAILURES =================================== ________________________________ test_less_than ________________________________ tests/test_compare.py:7: in test_less_than assert i1 < i2 E TypeError: '<' not supported between instances of 'Task' and 'Task' =========================== short test summary info ============================ FAILED tests/test_compare.py::test_less_than - TypeError: '<' not supported between instances of 'Task' and 'Task' ========================= 1 failed, 1 passed in 0.03s ========================== Der Fehler liegt einfach daran, dass wir diese Funktion noch nicht implementiert haben. Dennoch müssen wir diesen Test nicht wieder wegwerfen; wir können ihn einfach auslassen: .. code-block:: python :emphasize-lines: 1, 6 import pytest from cusy.tasks import Task @pytest.mark.skip(reason="Tasks do not yet allow a < comparison") def test_less_than(): i1 = Task("Update pytest section") i2 = Task("Update cibuildwheel section") assert i1 < i2 Der Marker ``@pytest.mark.skip()`` weist pytest an, den Test zu überspringen. Die Angabe eines Grundes ist zwar optional, aber sie hilft bei der weiteren Entwicklung. Wenn wir übersprungene Tests ausführen, werden sie als ``s`` angezeigt: .. code-block:: pytest :emphasize-lines: 6 $ uv run pytest --tb=short tests/test_compare.py ============================= test session starts ============================== … collected 2 items tests/test_compare.py s. [100%] ========================= 1 passed, 1 skipped in 0.00s ========================= oder verbos als ``SKIPPED``: .. code-block:: pytest :emphasize-lines: 1, 10 $ uv run pytest -v -ra tests/test_compare.py ============================= test session starts ============================== … collected 2 items tests/test_compare.py::test_less_than SKIPPED (Tasks do not yet allo...) [ 50%] tests/test_compare.py::test_equality PASSED [100%] =========================== short test summary info ============================ SKIPPED [1] tests/test_compare.py:6: Tasks do not yet allow a < comparison ========================= 1 passed, 1 skipped in 0.00s ========================= Da wir pytest mit ``-r`` angewiesen haben, eine kurze Zusammenfassung unserer Tests auszugeben, erhalten wir eine zusätzliche Zeile am unteren Ende, die den Grund auflistet, den wir im Marker angegeben haben. Das ``a`` in ``-ra`` steht für *all except passed*. Die Optionen ``-ra`` sind die gebräuchlichsten, da wir fast immer wissen wollen, warum bestimmte Tests nicht bestanden haben. .. seealso:: * `Skipping test functions `_ Bedingtes Überspringen von Tests mit ``@pytest.mark.skipif`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Angenommen, wir wissen, dass wir die Sortierung in den Versionen 0.1.x der App cusy.tasks nicht unterstützen werden, wohl aber in Version 0.2.x. Dann können wir pytest anweisen, den Test für alle Versionen von Tasks, die kleiner als 0.2.x sind, wie folgt zu überspringen: .. code-block:: python :emphasize-lines: 2, 4, 8-11 import pytest from packaging.version import parse from cusy import tasks from cusy.tasks import Task @pytest.mark.skipif( parse(tasks.__version__).minor < 2, reason="The comparison with < is not yet supported in version 0.1.x.", ) def test_less_than(): i1 = Task("Update pytest section") i2 = Task("Update cibuildwheel section") assert i1 < i2 Mit dem ``skipif``-Marker könnt ihr beliebig viele Bedingungen eingeben, und wenn eine davon wahr ist, wird der Test übersprungen. In unserem Fall verwenden wir ``packaging.version.parse``, um die Minor-Version zu isolieren und sie mit der Zahl 2 zu vergleichen. In diesem Beispiel wird als zusätzliches Paket `packaging `_ verwendet. Wenn ihr das Beispiel ausprobieren möchtet, installiert es zunächst mit ``uv add --group dev packaging``. .. tip:: ``skipif`` ist auch hervorragend geeignet, wenn Tests für verschiedene Betriebssysteme unterschiedlich geschrieben werden müssen. .. seealso:: * `skipif `_ ``@pytest.mark.xfail`` ~~~~~~~~~~~~~~~~~~~~~~ Wenn wir alle Tests durchführen wollen, auch die, von denen wir wissen, dass sie fehlschlagen werden, können wir den Marker ``xfail`` oder genauer :samp:`@pytest.mark.xfail({CONDITION}, {... *, {REASON}, run={True}, raises={None}, strict={True})` verwenden. Der erste Satz von Parametern für diese Fixture ist der gleiche wie bei ``skipif``. ``run`` Der Test wird standardmäßig ausgeführt, außer wenn ``run=False`` gesetzt ist. ``raises`` erlaubt euch, einen Ausnahmetyp oder ein Tupel von Ausnahmetypen anzugeben, die zu einem ``xfail`` führen sollen. Jede andere Ausnahme führt dazu, dass der Test fehlschlägt. ``strict`` teilt pytest mit, ob bestandene Tests ``(strict=False)`` als ``XPASS`` oder mit ``strict=True`` als ``FAIL`` markiert werden sollen. Schauen wir uns ein Beispiel an: .. code-block:: python :emphasize-lines: 8-15, 17-21, 23- import pytest from packaging.version import parse from cusy import tasks from cusy.tasks import Task @pytest.mark.xfail( parse(tasks.__version__).minor < 2, reason="The comparison with < is not yet supported in version 0.1.x.", ) def test_less_than(): i1 = Task("Update pytest section") i2 = Task("Update cibuildwheel section") assert i1 < i2 @pytest.mark.xfail(reason="Feature #17: not implemented yet") def test_xpass(): i1 = Task("Update pytest section") i2 = Task("Update pytest section") assert i1 == i2 @pytest.mark.xfail(reason="Feature #17: not implemented yet", strict=True) def test_strict_xfail(): i1 = Task("Update pytest section") i2 = Task("Update pytest section") assert i1 == i2 Wir haben hier drei Tests: einen, von dem wir wissen, dass er fehlschlägt, und zwei, von denen wir wissen, dass sie bestanden wird. Diese Tests demonstrieren sowohl das Scheitern als auch das Bestehen der Verwendung von ``xfail`` und die Auswirkungen der Verwendung von ``strict``. Das erste Beispiel verwendet auch den optionalen Parameter ``condition``, der wie die Bedingungen von ``skipif`` funktioniert. Und so sieht das Ergebnis aus: .. code-block:: $ uv run pytest -v -ra tests/test_xfail.py ============================= test session starts ============================== … collected 3 items tests/test_xfail.py::test_less_than XFAIL (The comparison with < is ...) [ 33%] tests/test_xfail.py::test_xpass XPASS (Feature #17: not implemented yet) [ 66%] tests/test_xfail.py::test_strict_xfail FAILED [100%] =================================== FAILURES =================================== ______________________________ test_strict_xfail _______________________________ [XPASS(strict)] Feature #17: not implemented yet =========================== short test summary info ============================ XFAIL tests/test_xfail.py::test_less_than - The comparison with < is not yet supported in version 0.1.x. XPASS tests/test_xfail.py::test_xpass Feature #17: not implemented yet FAILED tests/test_xfail.py::test_strict_xfail =================== 1 failed, 1 xfailed, 1 xpassed in 0.02s ==================== Tests, die mit ``xfail`` gekennzeichnet sind: - Nicht bestandene Tests werden mit ``XFAIL`` angezeigt. - Bestandene Tests mit ``strict=False`` führen zu ``XPASSED``. - Bestandene Tests mit ``strict=True`` führen zu ``FAILED``. Wenn ein Test fehlschlägt, der mit ``xfail`` markiert ist, also mit ``XFAIL`` ausgegeben wird, hatten wir Recht in der Annahme, dass der Test fehlschlagen wird. Bei Tests, die mit ``xfail`` markiert wurden, jedoch tatsächlich bestanden wurden, gibt es zwei Möglichkeiten: Wenn sie zu ``XFAIL`` führen sollen, dann solltet ihr die Finger von ``strict`` lassen. Wenn sie hingegen ``FAILED`` ausgeben sollen, dann setzt ``strict``. Ihr könnt ``strict`` entweder als Option für den ``xfail``-Marker setzen, wie wir es in diesem Beispiel getan haben, oder ihr könnt es auch global mit der Einstellung ``strict_xfail = true`` in der pytest-Konfigurationsdatei :file:`pyproject.toml` setzen. Ein pragmatischer Grund, immer ``strict_xfail = true`` zu verwenden, ist, dass wir uns alle fehlgeschlagenen Tests üblicherweise genauer anzuschauen. Und so sehen wir uns dann auch die Fälle an, in denen die Erwartungen an den Test nicht mit dem Ergebnis übereinstimmen. ``strict_xfail`` kann sehr hilfreich sein wenn ihr in einer testgetriebenen Entwicklung arbeitet und ihr Testfälle schreibt, von denen ihr wisst, dass sie noch nicht implementiert sind, die ihr aber in Kürze implementieren wollt. Lasst dabei die ``strict_xfail``-Tests auf dem Feature-Branch, in dem die Funktion implementiert wird. Oder etwas geht kaputt, ein oder mehrere Test schlagen fehl, und ihr könnt nicht sofort sofort an der Behebung arbeiten. Das Markieren der Tests als ``xfail``, ``strict_xfail = true`` mit der Angabe der Fehler-/Issue-Report-ID in ``reason``, ist eine gute Möglichkeit, den Test weiterlaufen zu lassen und ihn nicht zu vergessen. Wenn ihr jedoch nur ein Brainstorming über die Behaviors eurer Anwendung macht, solltet ihr noch keine Tests schreiben und sie mit ``strict_xfail`` oder ``skip`` markieren: hier würde ich euch :abbr:`YAGNI (‘You Aren’t Gonna Need It’, deutsch: „Du wirst es nicht brauchen“)` entgegenhalten. Implementiert Dinge immer erst dann, wenn sie tatsächlich gebraucht werden und niemals, wenn ihr nur ahnt, dass ihr sie brauchen werdet. .. tip:: * Ihr solltet :samp:`strict_xfail = true` in :file:`pyproject.toml` setzen, um alle ``XPASSED``-Ergebnisse in ``FAILED`` zu verwandeln. * Zudem solltet ihr immer :samp:`-ra` oder zumindest :samp:`-rxX` verwenden um euch den Grund anzeigen zu lassen. * Und schließlich solltet ihr eine Fehlernummer in ``reason`` angeben. * ``pytest --runxfail`` ignoriert grundsätzlich die ``xfail``-Marker. Dies ist sehr nützlich in den letzten Phasen des Pre-Production-Testing. .. _select-tests-with-markers: Auswahl von Tests mit eigenen Markern ------------------------------------- Eigene Marker könnt ihr euch wie Tags oder Etiketten vorstellen. Sie können verwendet werden, um Tests auszuwählen, die ausgeführt oder übersprungen werden sollen. Nehmen wir an, wir wollen einige unserer Tests mit ``smoke`` kennzeichnen. Die Segmentierung einer Teilmenge von Tests in eine Smoke-Test-Suite ist eine gängige Praxis, um einen repräsentativen Satz von Tests ausführen zu können, der uns schnell sagen kann, ob irgendetwas mit einem der Hauptsysteme nicht in Ordnung ist. Darüber hinaus werden wir einige unserer Tests mit ``exception`` kennzeichnen – diejenigen, die auf erwartete Ausnahmen prüfen: .. code-block:: python :emphasize-lines: 6 import pytest from cusy.tasks import InvalidTaskId, Task @pytest.mark.smoke def test_start(tasks_db): """ Change state from ‘todo’ to ‘in progress’ """ i = tasks_db.add_task(Task("Update pytest section", state="todo")) tasks_db.start(i) s = tasks_db.get_task(i) assert s.state == "in progress" Jetzt sollten wir in der Lage sein, nur diesen Test auszuwählen, indem wir die Option ``-m smoke`` verwenden: .. code-block:: pytest $ uv run pytest -v -m smoke tests/test_start.py ============================= test session starts ============================== … collected 2 items / 1 deselected / 1 selected tests/test_start.py::test_start PASSED [100%] =============================== warnings summary =============================== tests/test_start.py:6 /Users/veit/cusy.tasks/tests/test_start.py:6: PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html @pytest.mark.smoke -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ================== 1 passed, 1 deselected, 1 warning in 0.00s ================== Nun konnten wir zwar nur einen Test durchzuführen, aber wir haben auch eine Warnung erhalten: ``PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo?`` Sie hilft, Tippfehler zu vermeiden. pytest möchte, dass wir benutzerdefinierte Marker registrieren, indem wir einen Marker-Abschnitt zu :file:`pyproject.toml` hinzufügen, :abbr:`z.B. (zum Beispiel)`: .. code-block:: toml [tool.pytest] markers = [ "smoke: Small subset of all tests", ] Jetzt warnt uns pytest nicht mehr vor einem unbekannten Marker: .. code-block:: pytest :emphasize-lines: 4 $ uv run pytest -v -m smoke tests/test_start.py ============================= test session starts ============================== … configfile: pyproject.toml collected 2 items / 1 deselected / 1 selected tests/test_start.py::test_start PASSED [100%] ======================= 1 passed, 1 deselected in 0.00s ======================== Machen wir dasselbe mit der ``exception``-Markierung für ``test_start_non_existent``. #. Zuerst registrieren wir den Marker in :file:`pyproject.toml`: .. code-block:: toml :emphasize-lines: 4 [tool.pytest] markers = [ "smoke: Small subset of tests", "exception: Only run expected exceptions", ] #. Dann fügen wir den Marker zum Test hinzu: .. code-block:: python :emphasize-lines: 1 @pytest.mark.exception def test_start_non_existent(tasks_db): """ Shouldn’t start a non-existent task. """ # any_number will be invalid, db is empty any_number = 44 with pytest.raises(InvalidTaskId): tasks_db.start(any_number) #. Schließlich führen wir den Test mit ``-m exception`` aus: .. code-block:: pytest $ uv run pytest -v -m exception tests/test_start.py ============================= test session starts ============================== … configfile: pyproject.toml collected 2 items / 1 deselected / 1 selected tests/test_start.py::test_start_non_existent PASSED [100%] ======================= 1 passed, 1 deselected in 0.01s ======================== Marker für Dateien, Klassen und Parameter ----------------------------------------- Mit den Tests in :file:`test_start.py` haben wir :samp:`@pytest.mark.{MARKER_NAME}`-Dekoratoren zu Testfunktionen hinzugefügt. Wir können auch ganze Dateien oder Klassen mit Markern versehen, um mehrere Tests zu markieren, oder in parametrisierte Tests hineingehen und einzelne Parametrisierungen markieren. Wir können sogar mehrere Marker auf einen einzigen Test setzen. Zunächst setzen wir in :file:`test_finish.py` mit einem Marker auf Dateiebene: .. code-block:: python :emphasize-lines: 5 import pytest from cusy.tasks import Task pytestmark = pytest.mark.finish Wenn pytest ein ``pytestmark``-Attribut in einem Testmodul sieht, wird es den oder die Marker auf alle Tests in diesem Modul anwenden. Wenn ihr mehr als einen Marker auf die Datei anwenden wollt, könnt ihr eine Listenform verwenden: :samp:`pytestmark = [pytest.mark.{MARKER_ONE}, pytest.mark.{MARKER_TWO}]`. Eine andere Möglichkeit, mehrere Tests gleichzeitig zu markieren, besteht darin, Tests in einer Klasse zu haben und Markierungen auf Klassenebene zu verwenden: .. code-block:: python :emphasize-lines: 1 @pytest.mark.smoke class TestFinish: def test_finish_from_todo(self, tasks_db): i = tasks_db.add_task(Task("Update pytest section", state="todo")) tasks_db.finish(i) s = tasks_db.get_task(i) assert s.state == "done" def test_finish_from_in_prog(self, tasks_db): i = tasks_db.add_task( Task("Update pytest section", state="in progress") ) tasks_db.finish(i) s = tasks_db.get_task(i) assert s.state == "done" def test_finish_from_done(self, tasks_db): i = tasks_db.add_task(Task("Update pytest section", state="done")) tasks_db.finish(i) s = tasks_db.get_task(i) assert s.state == "done" Die Testklasse :class:`TestFinish` ist mit ``@pytest.mark.smoke`` gekennzeichnet. Wenn ihr eine Testklasse auf diese Weise markiert, wird jede Testmethode in der Klasse mit dem gleichen Marker versehen. Wir können auch nur bestimmte Testfälle eines parametrisierten Tests markieren: .. code-block:: python :emphasize-lines: 5 @pytest.mark.parametrize( "states", [ "todo", pytest.param("in progress", marks=pytest.mark.smoke), "done", ], ) def test_finish(tasks_db, start_state): i = tasks_db.add_task(Task("Update pytest section", state=states)) tasks_db.finish(i) s = tasks_db.get_task(i) assert s.state == "done" Die :func:`test_finish`-Funktion ist nicht direkt markiert, sondern nur einer ihrer Parameter: :samp:`pytest.param("in progress", marks=pytest.mark.smoke)`. Ihr könnt mehr als einen Marker verwenden, indem ihr die Listenform verwendet: :samp:`marks=[pytest.mark.{ONE}, pytest.mark.{TWO}]`. Wenn ihr alle Testfälle eines parametrisierten Tests markieren wollt, fügt ihr den Marker wie bei einer normalen Funktion entweder über oder unter dem Dekorator ``parametrize`` ein. Das vorherige Beispiel bezog sich auf die Funktionsparametrisierung. Ihr könnt jedoch auch Fixtures auf die gleiche Weise markieren: .. code-block:: python :emphasize-lines: 8-9, 12 @pytest.fixture( params=[ "todo", pytest.param("in progress", marks=pytest.mark.smoke), "done", ] ) def start_state_fixture(request): return request.param def test_finish(tasks_db, start_state_fixture): i = tasks_db.add_task( Task("Update pytest section", state=start_state_fixture) ) tasks_db.finish(i) s = tasks_db.get_task(i) assert s.state == "done" Wenn ihr einer Funktion mehr als eine Markierung hinzufügen wollt, könnt ihr einfach stapeln. Zum Beispiel wird :func:`test_finish_non_existent` sowohl mit ``@pytest.mark.smoke`` als auch mit ``@pytest.mark.exception`` markiert: .. code-block:: python :emphasize-lines: 4-5 from cusy.tasks import InvalidTaskId, Task @pytest.mark.smoke @pytest.mark.exception def test_finish_non_existent(tasks_db): i = 44 # any_number will be invalid, db is empty with pytest.raises(InvalidTaskId): tasks_db.finish(i) Wir haben in :file:`test_finish.py` eine Reihe von Markern auf verschiedene Weise hinzugefügt. Dabei verwenden wir die Marker, um die auszuführenden Tests anstatt eine Testdatei auszuwählen: .. code-block:: pytest $ uv run pytest -v -m exception ============================= test session starts ============================== … configfile: pyproject.toml collected 36 items / 34 deselected / 2 selected test_finish.py::test_finish_non_existent PASSED [ 50%] test_start.py::test_start_non_existent PASSED [100%] ======================= 2 passed, 34 deselected in 0.07s ======================= Marker zusammen mit ``and``, ``or``, ``not`` und ``()`` ------------------------------------------------------- Wir können Marker logisch verknüpfen, um Tests auszuwählen, genau wie wir ``-k`` zusammen mit Schlüsselwörtern zur Auswahl von Testfällen in :ref:`Testsuite ` verwendet haben. So können wir nur die ``finish``-Tests, die sich mit ``exception`` befassen: .. code-block:: pytest $ uv run pytest -v -m "finish and exception" ============================= test session starts ============================== … configfile: pyproject.toml collected 36 items / 35 deselected / 1 selected test_finish.py::test_finish_non_existent PASSED [100%] ======================= 1 passed, 35 deselected in 0.08s ======================= Wir können auch alle logischen Verknüpfungen zusammen verwenden: .. code-block:: pytest $ uv run pytest -v -m "(exception or smoke) and (not finish)" ============================= test session starts ============================== … configfile: pyproject.toml collected 36 items / 34 deselected / 2 selected test_start.py::test_start PASSED [ 50%] test_start.py::test_start_non_existent PASSED [100%] ======================= 2 passed, 34 deselected in 0.08s ======================= Schließlich können wir auch Marker und Keywords für die Auswahl kombinieren, :abbr:`z.B. (zum Beispiel)` um Smoke-Tests auszuführen, die nicht Teil der Klasse :class:`TestFinish` sind: .. code-block:: console $ uv run pytest -v -m smoke -k "not TestFinish" ============================= test session starts ============================== … configfile: pyproject.toml collected 36 items / 33 deselected / 3 selected test_finish.py::test_finish[in progress] PASSED [ 33%] test_finish.py::test_finish_non_existent PASSED [ 66%] test_start.py::test_start PASSED [100%] ======================= 3 passed, 33 deselected in 0.07s ======================= Bei der Verwendung von Markern und Keywords ist zu beachten, dass die Namen der Marker bei der Option :samp:`-m {MARKERNAME}` vollständig sein müssen, während Keywords bei der Option :samp:`-k {KEYWORD}` eher einen Substring darstellen. ``--strict-markers`` -------------------- Üblicherweise erhalten wir eine Warnung, wenn ein Marker nicht registriert ist. Wenn diese Warnung stattdessen ein Fehler sein soll, können wir die Option ``--strict-markers`` verwenden. Dies hat zwei Vorteile: #. Der Fehler wird bereits ausgegeben, wenn die auszuführenden Tests gesammelt werden und nicht erst zur Laufzeit. Wenn ihr eine Testsuite habt, die länger als ein paar Sekunden dauert, werdet ihr es zu schätzen wissen, wenn ihr diese Rückmeldung schnell erhaltet. #. Zweitens sind Fehler manchmal leichter zu erkennen als Warnungen, besonders in Systemen mit :term:`kontinuierlicher Integration `. .. tip:: Es empfiehlt sich daher, immer ``--strict-markers`` zu verwenden. Anstatt die Option jedoch immer wieder einzugeben, könnt ihr ``--strict-markers`` in den Abschnitt ``addopts`` der :file:`pyproject.toml` einfügen: .. code-block:: toml :emphasize-lines: 3-4 [tool.pytest] ... addopts = [ "--strict-markers", ] .. _marker_fixtures_combined: Marker mit Fixtures kombinieren ------------------------------- Marker können in Verbindung mit Fixtures, Plugins und Hook-Funktionen verwendet werden. Die Built-in-Marker benötigen Parameter, während die benutzerdefinierten Marker, die wir bisher verwendet haben, keine Parameter benötigen. Erstellen wir einen neuen Marker namens ``num_tasks``, den wir an die ``tasks_db-Fixture`` übergeben können. Die ``tasks_db``-Fixture bereinigt derzeit die Datenbank für jeden Test, der sie verwenden möchte: .. code-block:: python @pytest.fixture(scope="function") def tasks_db(session_tasks_db): db = session_tasks_db db.delete_all() return db Wenn wir zum Beispiel vier Tasks in der Datenbank haben wollen, wenn unser Test beginnt, können wir einfach eine andere, aber ähnliche Fixture schreiben: .. code-block:: python @pytest.fixture(scope="session") def tasks_list(): """List of different Task objects""" return [ tasks.Task("Add Python 3.12 static type improvements", "veit", "todo"), tasks.Task("Add tips for efficient testing", "veit", "wip"), tasks.Task("Update cibuildwheel section", "veit", "done"), tasks.Task("Add backend examples", "veit", "done"), ] @pytest.fixture(scope="function") def populated_db(tasks_db, tasks_list): """TasksDB object populated with 'tasks_list'""" for i in tasks_list: tasks_db.add_task(i) return tasks_db Dann könnten wir die ursprüngliche Fixture für Tests verwenden, die eine leere Datenbank bereitstellt, und die neue Fixture für Tests, die eine Datenbank mit vier Tasks enthält: .. code-block:: python def test_zero_task(tasks_db): assert tasks_db.count() == 0 def test_four_tasks(populated_db): assert populated_db.count() == 4 Wir haben nun die Möglichkeit, entweder null oder vier Tasks in der Datenbank zu testen. Was aber, wenn wir keine, vier oder 13 Tasks haben wollen? Dann wollen wir nicht jedesmal eine neue Fixture schreiben. Marker erlauben uns, einem Test zu sagen, wieviele Tasks wir haben wollen. Hierfür sind drei Schritte notwendig: #. Zunächst definieren wir drei verschiedene Tests in :file:`test_tasks.py` mit unserem Marker ``@pytest.mark.num_tasks``: .. code-block:: python @pytest.mark.num_tasks def test_zero_task(tasks_db): assert tasks_db.count() == 0 @pytest.mark.num_tasks(4) def test_four_tasks(tasks_db): assert tasks_db.count() == 4 @pytest.mark.num_tasks(13) def test_thirteen_tasks(tasks_db): assert tasks_db.count() == 13 #. Diesen Marker müssen wir dann in der :file:`pyproject.toml`-Datei deklarieren: .. code-block:: toml :emphasize-lines: 4 [tool.pytest] markers = [ "…", "num_tasks: Number of tasks to be pre-filled for the tasks_db fixture", ] #. Nun modifizieren wir die ``tasks_db``-Fixture in der :file:`conftest.py`-Datei, um den Marker verwenden zu können. Um die Task-Informationen nicht hart kodieren zu müssen, werden wir das Python-Paket `Faker `_ verwenden, das wir mit ``uv add --group tests faker`` installieren können: .. code-block:: python :linenos: :emphasize-lines: 5, 13- import os from pathlib import Path from tempfile import TemporaryDirectory import faker import pytest from cusy import tasks ... @pytest.fixture(scope="function") def tasks_db(session_tasks_db, request, faker): db = session_tasks_db db.delete_all() # Support for random selection "@pytest.mark.num_tasks({NUMBER})`. faker.seed_instance(99) m = request.node.get_closest_marker("num_tasks") if m and len(m.args) > 0: num_tasks = m.args[0] for _ in range(num_tasks): db.add_task( Task(summary=faker.sentence(), owner=faker.first_name()) ) return db Hier gibt es eine Menge Änderungen, die wir jetzt durchgehen wollen. Zeile 13 Wir haben ``request`` und ``faker`` in die Liste der ``tasks_db``-Parameter aufgenommen. Zeile 18 Dies setzt die Zufälligkeit von Faker, so dass wir jedes Mal die gleichen Daten erhalten. Dabei verwenden wir Faker hier nicht für sehr zufällige Daten, sondern um zu vermeiden, dass wir selbst Daten erfinden müssen. Zeile 19 Hier verwenden wir ``request``, genauer ``request.node`` für die pytest-Repräsentation eines Tests. ``get_closest_marker('num_tasks')`` gibt ein Marker-Objekt zurück, wenn der Test mit ``num_tasks`` markiert ist, andernfalls gibt es ``None`` zurück. Die :func:`get_closest_marker`-Funktion gibt den Marker zurück, der dem Test am nächsten liegt, und das ist normalerweise das, was wir wollen. Zeile 20 Der Ausdruck ist wahr, wenn der Test mit ``num_tasks`` markiert ist und ein Argument angegeben wird. Die zusätzliche ``len``-Prüfung dient dazu, dass, falls jemand versehentlich nur ``pytest.mark.num_tasks`` verwendet, ohne die Anzahl der Tasks anzugeben, dieser Teil übersprungen wird. Zeile 21–24 Sobald wir wissen, wie viele Tasks wir erstellen müssen, lassen wir Faker einige Daten für uns erstellen. Faker stellt die Faker-Fixture zur Verfügung. * Für das Feld ``summary`` funktioniert die Methode :func:`faker.sentence`. * Für das Feld ``Owner`` funktioniert die Methode :func:`faker.first_name`. .. seealso:: * Es gibt noch viele andere Möglichkeiten, die ihr mit Faker nutzen könnt. Schaut hierfür in die `Faker-Dokumentation `_. * Neben Faker gibt es nach weitere Bibliotheken, die Fake-Daten bereitstellen, siehe :ref:`Fake Plugins `. Führen wir die Tests nun aus, um sicherzustellen, dass alles richtig funktioniert: .. code-block:: pytest $ uv run pytest -v -s test_tasks.py ============================= test session starts ============================== … configfile: pyproject.toml plugins: Faker-19.10.0 collected 3 items test_tasks.py::test_zero_task PASSED test_tasks.py::test_four_tasks PASSED test_tasks.py::test_thirteen_tasks PASSED ============================== 3 passed in 0.09s =============================== .. note:: Damit ihr einen Eindruck bekommt, wie die Daten von Faker aussehen, könnt ihr eine ``print``-Anweisung zu :func:`test_four_tasks` hinzufügen: .. code-block:: python :emphasize-lines: 4- @pytest.mark.num_tasks(4) def test_four_tasks(tasks_db): assert tasks_db.count() == 4 print() for i in tasks_db.list_tasks(): print(i) Anschließend könnt ihr die Tests in :file:`test_tasks.py` erneut aufrufen: .. code-block:: pytest :emphasize-lines: 10-13 $ uv run pytest -v -s test_tasks.py ============================= test session starts ============================== … configfile: pyproject.toml plugins: Faker-19.10.0 collected 3 tasks test_tasks.py::test_zero_task PASSED test_tasks.py::test_four_tasks Task(summary='Herself outside discover card beautiful rock.', owner='Alyssa', state='todo', id=1) Task(summary='Bed perhaps current reveal open society small.', owner='Lynn', state='todo', id=2) Task(summary='Charge produce sure full water.', owner='Allison', state='todo', id=3) Task(summary='Light I especially account.', owner='James', state='todo', id=4) PASSED test_tasks.py::test_thirteen_tasks PASSED ============================== 3 passed in 0.09s =============================== Marker generieren ----------------- Angenommen, ihr habt eine Testsuite, die Tests für bestimmte Plattformen markiert, nämlich ``pytest.mark.darwin``, ``pytest.mark.win32`` :abbr:`usw. (und so weiter)`, und ihr habt auch Tests, die auf allen Plattformen laufen und keine spezifische Markierung haben. Wenn ihr nun eine Möglichkeit sucht, nur die Tests für eure bestimmte Plattform auszuführen, könnt ihr folgendes verwenden: .. code-block:: python :caption: conftest.py import sys import pytest ALL = {"win32", "darwin", "linux"} def pytest_setup(item): supported_platforms = ALL.intersection( mark.name for mark in item.iter_markers() ) pf = sys.platform if supported_platforms and pf not in supported_platforms: pytest.skip(f"cannot run on platform {pf}") Dies führt dazu, dass Tests übersprungen werden, wenn sie für eine andere Plattform angegeben wurden. Nun erstellen wir eine kleine Testdatei, um zu zeigen, wie das aussieht: .. code-block:: python :caption: test_platform.py import pytest def test_foo_everywhere(): pass @pytest.mark.win32 def test_foo_on_win32(): pass @pytest.mark.darwin def test_foo_on_darwin(): pass @pytest.mark.linux def test_foo_on_linux(): pass Nun können wir pytest ausführen und uns die Begründungen für die übersprungenen Tests anzeigen lassen: .. code-block:: pytest $ uv run pytest -rs tests/test_platform.py ============================= test session starts ============================== platform darwin -- Python 3.14.0b4, pytest-8.4.1, pluggy-1.6.0 ... collected 4 items tests/test_platform.py ..ss [100%] =========================== short test summary info ============================ SKIPPED [2] tests/conftest.py:20: cannot run on platform darwin ========================= 2 passed, 2 skipped in 0.03s ========================= oder spezifischer: .. code-block:: pytest $ uv run pytest -m darwin -rs tests/test_platform.py ============================= test session starts ============================== platform darwin -- Python 3.14.0b4, pytest-8.4.1, pluggy-1.6.0 ... collected 4 items / 3 deselected / 1 selected tests/test_platform.py . [100%] ======================= 1 passed, 3 deselected in 0.02s ======================== Marker basierend auf Testnamen ------------------------------ Alternativ können Marker auch über die Namen der Testfunktionen angegeben werden, indem ihr einen Hook implementiert, der automatisch Marker definiert: .. code-block:: python :caption: test_platform.py def test_foo_everywhere(): pass def test_foo_on_win32(): pass def test_foo_on_darwin(): pass def test_foo_on_linux(): pass Nun definieren wir in der :file:`conftest.py` in `pytest_collection_modifyitems `_ drei Marker dynamisch: .. code-block:: python :caption: conftest.py import pytest def pytest_collection_modifyitems(items): for item in items: if "win32" in item.nodeid: item.add_marker(pytest.mark.win32) elif "darwin" in item.nodeid: item.add_marker(pytest.mark.darwin) elif "linux" in item.nodeid: item.add_marker(pytest.mark.linux) Nun können wir die Option ``-m`` verwenden, um einen Satz auszuwählen: .. code-block:: pytest $ uv run pytest -m darwin ============================= test session starts ============================== platform darwin -- Python 3.14.0, pytest-9.0.1, pluggy-1.6.0 ... collected 4 items / 3 deselected / 1 selected tests/test_platform.py . [100%] ======================= 1 passed, 3 deselected in 0.00s ======================== Marker auflisten ---------------- Wir haben bereits eine Menge Marker behandelt: die Built-in-Marker ``skip``, ``skipif`` und ``xfail``, unsere eigenen Marker ``smoke``, ``exception``, ``finish`` und ``num_tasks`` und es gibt auch noch ein paar weitere Built-in-Marker. Und wenn wir anfangen, :doc:`plugins` zu verwenden, können noch weitere Marker hinzukommen. Um alle verfügbaren Marker mit Beschreibungen und Parameter aufzulisten, könnt ihr ``pytest --markers`` ausführen: .. code-block:: pytest $ pytest --markers @pytest.mark.exception: Only run expected exceptions @pytest.mark.finish: Only run finish tests @pytest.mark.smoke: Small subset of all tests @pytest.mark.num_tasks: Number of tasks to be pre-filled for the tasks_db fixture @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 … Dies ist eine sehr praktische Funktion, mit der wir schnell nach Markern suchen können, und ein guter Grund, nützliche Beschreibungen zu unseren eigenen Markern hinzuzufügen.