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.
Siehe auch
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)
alsXPASS
oder mitstrict=True
alsFAIL
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 zuXPASSED
.Bestandene Tests mit
strict=True
führen zuFAILED
.
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
inpytest.ini
setzen, um alleXPASSED
-Ergebnisse inFAILED
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 diexfail
-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
.
Zuerst registrieren wir den Marker in
pytest.ini
:[pytest] markers = smoke: Small subset of tests exception: Only run expected exceptions
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)
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:
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 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:
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
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
Nun modifizieren wir die
items_db
-Fixture in derconftest.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 mitpython -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
undfaker
in die Liste deritems_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
, genauerrequest.node
für die pytest-Repräsentation eines Tests.get_closest_marker('num_items')
gibt ein Marker-Objekt zurück, wenn der Test mitnum_items
markiert ist, andernfalls gibt esNone
zurück. Dieget_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ätzlichelen
-Prüfung dient dazu, dass, falls jemand versehentlich nurpytest.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 Methodefaker.sentence()
.Für das Feld
Owner
funktioniert die Methodefaker.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.