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 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 Funktionen parametrisieren 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:

@pytest.mark.filterwarnings(WARNUNG)

Dieser Marker fügt dem angegebenen Test einen Warnfilter hinzu.

@pytest.mark.skip(reason=None)

Mit diesem Marker wird der Test mit einem optionalen Grund übersprungen.

@pytest.mark.skipif(BEDINGUNG, ...*, GRUND)

Diese Markierung überspringt den Test, wenn eine der Bedingungen True ist.

@pytest.mark.xfail(BEDINGUNG, ...* GRUND, run=True, raises=None, strict=xfail_strict)

Dieser Marker teilt pytest mit, dass wir das Fehlschlagen erwarten.

@pytest.mark.parametrize({ARG1, ARG2, ...

Dieser Marker ruft eine Testfunktion mehrfach auf, wobei nacheinander verschiedene Argumente übergeben werden.

@pytest.mark.usefixtures({FIXTURE1, FIXTURE2, ...

Dieser Marker kennzeichnet Tests, die alle angegebenen Fixtures benötigen.

@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 Items-Anwendung die Möglichkeit zum Sortieren hinzufügen und möchten, dass die Item-Klasse Vergleiche unterstützt. Wir schreiben einen Test für den Vergleich von Item-Objekten mit < wie folgt:

from items import Item


def test_less_than():
    i1 = Item("Update pytest section")
    i2 = Item("Update cibuildwheel section")
    assert i1 < i2


def test_equality():
    i1 = Item("Update pytest section")
    i2 = Item("Update pytest section")
    assert i1 == i2

Und er scheitert:

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 'Item' and 'Item'
=========================== short test summary info ============================
FAILED tests/test_compare.py::test_less_than - TypeError: '<' not supported between instances of 'Item' and 'Item'
========================= 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:

 import pytest

 from items import Item


 @pytest.mark.skip(reason="Items do not yet allow a < comparison")
 def test_less_than():
     i1 = Item("Update pytest section")
     i2 = Item("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:

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

 $ pytest -v -ra tests/test_compare.py
 ============================= test session starts ==============================
 ...
 collected 2 items

 tests/test_compare.py::test_less_than SKIPPED (Items do not yet allo...) [ 50%]
 tests/test_compare.py::test_equality PASSED                              [100%]

 =========================== short test summary info ============================
 SKIPPED [1] tests/test_compare.py:6: Items 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.

Bedingtes Überspringen von Tests mit @pytest.mark.skipif

Angenommen, wir wissen, dass wir die Sortierung in den Versionen 0.1.x der App Items nicht unterstützen werden, wohl aber in Version 0.2.x. Dann können wir pytest anweisen, den Test für alle Versionen von Items, die kleiner als 0.2.x sind, wie folgt zu überspringen:

 import pytest
 from packaging.version import parse

 import items
 from items import Item


 @pytest.mark.skipif(
 parse(items.__version__).minor < 2,
 reason="The comparison with < is not yet supported in version 0.1.x.",
 )
 def test_less_than():
     i1 = Item("Update pytest section")
     i2 = Item("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 python -m pip install packaging.

Tipp

skipif ist auch hervorragend geeignet, wenn Tests für verschiedene Betriebssysteme unterschiedlich geschrieben werden müssen.

Siehe auch

@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 @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:

 import pytest
 from packaging.version import parse

 import items
 from items import Item


 @pytest.mark.xfail(
 parse(items.__version__).minor < 2,
 reason="The comparison with < is not yet supported in version 0.1.x.",
 )
 def test_less_than():
     i1 = Item("Update pytest section")
     i2 = Item("Update cibuildwheel section")
     assert i1 < i2

 @pytest.mark.xfail(reason="Feature #17: not implemented yet")
 def test_xpass():
     i1 = Item("Update pytest section")
     i2 = Item("Update pytest section")
     assert i1 == i2

 @pytest.mark.xfail(reason="Feature #17: not implemented yet", strict=True)
 def test_xfail_strict():
     i1 = Item("Update pytest section")
     i2 = Item("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:

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_xfail_strict FAILED                            [100%]

=================================== FAILURES ===================================
______________________________ test_xfail_strict _______________________________
[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_xfail_strict
=================== 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 xfail_strict=True in der pytest-Konfigurationsdatei pytest.ini setzen.

Ein pragmatischer Grund, immer xfail_strict=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.

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 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=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 xfail oder skip markieren: hier würde ich euch YAGNI entgegenhalten. Implementiert Dinge immer erst dann, wenn sie tatsächlich gebraucht werden und niemals, wenn ihr nur ahnt, dass ihr sie brauchen werdet.

Tipp

  • Ihr solltet xfail_strict = True in pytest.ini setzen, um alle XPASSED-Ergebnisse in FAILED zu verwandeln.

  • Zudem solltet ihr immer -ra oder zumindest -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.

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:

 import pytest

 from items import InvalidItemId, Item


 @pytest.mark.smoke
 def test_start(items_db):
     """
     Change state from ‘todo’ to ‘in progress’
     """
     i = items_db.add_item(Item("Update pytest section", state="todo"))
     items_db.start(i)
     s = items_db.get_item(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:

$ 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/items/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 pytest.ini hinzufügen, z.B.:

[pytest]
markers =
    smoke: Small subset of all tests

Jetzt warnt uns pytest nicht mehr vor einem unbekannten Marker:

 $ pytest -v -m smoke tests/test_start.py
 ============================= test session starts ==============================
 ...
 configfile: pytest.ini
 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.

  1. Zuerst registrieren wir den Marker in pytest.ini:

    [pytest]
    markers =
        smoke: Small subset of tests
        exception: Only run expected exceptions
    
  2. Dann fügen wir den Marker zum Test hinzu:

    @pytest.mark.exception
    def test_start_non_existent(items_db):
        """
        Shouldn’t start a non-existent item.
        """
        # any_number will be invalid, db is empty
        any_number = 44
    
        with pytest.raises(InvalidItemId):
            items_db.start(any_number)
    
  3. Schließlich führen wir den Test mit -m exception aus:

    $ pytest -v -m exception tests/test_start.py
    ============================= test session starts ==============================
    ...
    configfile: pytest.ini
    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 test_start.py haben wir @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 test_finish.py mit einem Marker auf Dateiebene:

 import pytest

 from items import Item

 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: 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:

 @pytest.mark.smoke
 class TestFinish:
     def test_finish_from_todo(self, items_db):
         i = items_db.add_item(Item("Update pytest section", state="todo"))
         items_db.finish(i)
         s = items_db.get_item(i)
         assert s.state == "done"

     def test_finish_from_in_prog(self, items_db):
         i = items_db.add_item(Item("Update pytest section", state="in progress"))
         items_db.finish(i)
         s = items_db.get_item(i)
         assert s.state == "done"

     def test_finish_from_done(self, items_db):
         i = items_db.add_item(Item("Update pytest section", state="done"))
         items_db.finish(i)
         s = items_db.get_item(i)
         assert s.state == "done"

Die Testklasse 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:

 @pytest.mark.parametrize(
     "states",
     [
         "todo",
         pytest.param("in progress", marks=pytest.mark.smoke),
         "done",
     ],
 )
 def test_finish(items_db, start_state):
     i = items_db.add_item(Item("Update pytest section", state=states))
     items_db.finish(i)
     s = items_db.get_item(i)
     assert s.state == "done"

Die test_finish()-Funktion ist nicht direkt markiert, sondern nur einer ihrer Parameter: pytest.param("in progress", marks=pytest.mark.smoke). Ihr könnt mehr als einen Marker verwenden, indem ihr die Listenform verwendet: 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:

 @pytest.fixture(
     params=[
         "todo",
         pytest.param("in progress", marks=pytest.mark.smoke),
         "done",
     ]
 )
 def start_state_fixture(request):
     return request.param


 def test_finish(items_db, start_state_fixture):
     i = items_db.add_item(Item("Update pytest section", state=start_state_fixture))
     items_db.finish(i)
     s = items_db.get_item(i)
     assert s.state == "done"

Wenn ihr einer Funktion mehr als eine Markierung hinzufügen wollt, könnt ihr einfach stapeln. Zum Beispiel wird test_finish_non_existent() sowohl mit @pytest.mark.smoke als auch mit @pytest.mark.exception markiert:

 from items import InvalidItemId, Item


 @pytest.mark.smoke
 @pytest.mark.exception
 def test_finish_non_existent(items_db):
     i = 44 # any_number will be invalid, db is empty
     with pytest.raises(InvalidItemId):
         items_db.finish(i)

Wir haben in 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:

$ cd tests
$ tests % pytest -v -m exception
============================= test session starts ==============================
...
configfile: pytest.ini
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 Testsuite verwendet haben. So können wir nur die finish-Tests, die sich mit exception befassen:

pytest -v -m "finish and exception"
============================= test session starts ==============================
...
configfile: pytest.ini
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:

 $ pytest -v -m "(exception or smoke) and (not finish)"
============================= test session starts ==============================
...
configfile: pytest.ini
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, z.B. um Smoke-Tests auszuführen, die nicht Teil der Klasse TestFinish sind:

$ pytest -v -m smoke -k "not TestFinish"
============================= test session starts ==============================
...
configfile: pytest.ini
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 -m MARKERNAME vollständig sein müssen, während Keywords bei der Option -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:

  1. 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.

  2. Zweitens sind Fehler manchmal leichter zu erkennen als Warnungen, besonders in Systemen mit kontinuierlicher Integration.

Tipp

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 pytest.ini einfügen:

[pytest]
...
addopts =
    --strict-markers

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_items, den wir an die items_db-Fixture übergeben können. Die items_db-Fixture bereinigt derzeit die Datenbank für jeden Test, der sie verwenden möchte:

@pytest.fixture(scope="function")
    def items_db(session_items_db):
    db = session_items_db
    db.delete_all()
    return db

Wenn wir zum Beispiel vier Items in der Datenbank haben wollen, wenn unser Test beginnt, können wir einfach eine andere, aber ähnliche Fixture schreiben:

@pytest.fixture(scope="session")
def items_list():
    """List of different Item objects"""
    return [
        items.Item("Add Python 3.12 static type improvements", "veit", "todo"),
        items.Item("Add tips for efficient testing", "veit", "wip"),
        items.Item("Update cibuildwheel section", "veit", "done"),
        items.Item("Add backend examples", "veit", "done"),
    ]


@pytest.fixture(scope="function")
def populated_db(items_db, items_list):
    """ItemsDB object populated with 'items_list'"""
    for i in items_list:
        items_db.add_item(i)
    return items_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 Items enthält:

def test_zero_item(items_db):
    assert items_db.count() == 0


def test_four_items(populated_db):
    assert populated_db.count() == 4

Wir haben nun die Möglichkeit, entweder null oder vier Items in der Datenbank zu testen. Was aber, wenn wir keine, vier oder 13 Items haben wollen? Dann wollen wir nicht jedesmal eine neue Fixture schreiben. Marker erlauben uns, einem Test zu sagen, wieviele Items wir haben wollen. Hierfür sind drei Schritte notwendig:

  1. Zunächst definieren wir drei verschiedene Tests in test_items.py mit dem unserem Marker @pytest.mark.num_items:

    @pytest.mark.num_items
    def test_zero_item(items_db):
        assert items_db.count() == 0
    
    
    @pytest.mark.num_items(4)
    def test_four_items(items_db):
        assert items_db.count() == 4
    
    
    @pytest.mark.num_items(13)
    def test_thirteen_items(items_db):
        assert items_db.count() == 13
    
  2. Diesen Marker müssen wir dann in der pytest.ini-Datei deklarieren:

    [pytest]
    markers =
        ...
        num_items: Number of items to be pre-filled for the items_db fixture
    
  3. Nun modifizieren wir die items_db-Fixture in der conftest.py-Datei, um den Marker verwenden zu können. Um die Item-Informationen nicht hart kodieren zu müssen, werden wir das Python-Paket Faker verwenden, das wir mit python -m pip install faker installieren können:

     1import os
     2from pathlib import Path
     3from tempfile import TemporaryDirectory
     4
     5import faker
     6import pytest
     7
     8import items
     9
    10...
    11
    12@pytest.fixture(scope="function")
    13def items_db(session_items_db, request, faker):
    14    db = session_items_db
    15    db.delete_all()
    16    # Support for random selection "@pytest.mark.num_items({NUMBER})`.
    17    faker.seed_instance(99)
    18    m = request.node.get_closest_marker("num_items")
    19    if m and len(m.args) > 0:
    20        num_items = m.args[0]
    21        for _ in range(num_items):
    22            db.add_item(Item(summary=faker.sentence(), owner=faker.first_name()))
    23    return db
    

    Hier gibt es eine Menge Änderungen, die wir jetzt durchgehen wollen.

    Zeile 13

    Wir haben request und faker in die Liste der items_db-Parameter aufgenommen.

    Zeile 17

    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 18

    Hier verwenden wir request, genauer request.node für die pytest-Repräsentation eines Tests. get_closest_marker('num_items') gibt ein Marker-Objekt zurück, wenn der Test mit num_items markiert ist, andernfalls gibt es None zurück. Die 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 19

    Der Ausdruck ist wahr, wenn der Test mit num_items markiert ist und ein Argument angegeben wird. Die zusätzliche len-Prüfung dient dazu, dass, falls jemand versehentlich nur pytest.mark.num_items verwendet, ohne die Anzahl der Items anzugeben, dieser Teil übersprungen wird.

    Zeile 20–22

    Sobald wir wissen, wie viele Items 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 faker.sentence().

    • Für das Feld Owner funktioniert die Methode faker.first_name().

    Siehe auch

    • 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 Bibliothkeen, die Fake-Daten bereitstellen, siehe Fake Plugins.

Führen wir die Tests nun aus, um sicherzustellen, dass alles richtig funktioniert:

$ pytest -v -s test_items.py
============================= test session starts ==============================
...
configfile: pytest.ini
plugins: Faker-19.10.0
collected 3 items

test_items.py::test_zero_item PASSED
test_items.py::test_four_items PASSED
test_items.py::test_thirteen_items PASSED

============================== 3 passed in 0.09s ===============================

Bemerkung

Damit ihr einen Eindruck bekommt, wie die Daten von Faker aussehen, könnt ihr eine print-Anweisung zu test_four_items() hinzufügen:

@pytest.mark.num_items(4)
def test_four_items(items_db):
    assert items_db.count() == 4
    print()
    for i in items_db.list_items():
        print(i)

Anschließend könnt ihr die Tests in test_items.py erneut aufrufen:

$ pytest -v -s test_items.py
============================= test session starts ==============================
...
configfile: pytest.ini
plugins: Faker-19.10.0
collected 3 items

test_items.py::test_zero_item PASSED
test_items.py::test_four_items
Item(summary='Herself outside discover card beautiful rock.', owner='Alyssa', state='todo', id=1)
Item(summary='Bed perhaps current reveal open society small.', owner='Lynn', state='todo', id=2)
Item(summary='Charge produce sure full water.', owner='Allison', state='todo', id=3)
Item(summary='Light I especially account.', owner='James', state='todo', id=4)
PASSED
test_items.py::test_thirteen_items PASSED

============================== 3 passed in 0.09s ===============================

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_items und es gibt auch noch ein paar weitere Built-in-Marker. Und wenn wir anfangen, 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:

$ 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_items: Number of items to be pre-filled for the items_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.