Test-Funktionen schreiben#
assert
-Anweisungen#
Wenn ihr Testfunktionen schreibt, ist die normale pytest-assert
-Anweisung
euer wichtigstes Werkzeug. Die Einfachheit dieser Anweisung bringt viele
Entwickler dazu, pytest gegenüber anderen Frameworks zu bevorzugen. Im Folgenden
findet ihr eine Liste einiger assert
-Formen und assert
-Hilfsfunktionen
von Unittest:
pytest |
unittest |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Mit pytest könnt ihr assert AUSDRUCK
mit einem beliebigen Ausdruck
verwenden. Wenn der Ausdruck bei einer Konvertierung in einen booleschen Wert zu
False
ausgewertet würde, würde der Test fehlschlagen.
pytest enthält eine Funktion namens assert rewriting
, die assert
-Aufrufe
abfängt und sie durch etwas ersetzt, das euch mehr darüber sagen kann, warum
eure Annahmen fehlgeschlagen sind. Sehen wir uns an, wie hilfreich dieses
Rewriting ist, indem wir uns einen fehlgeschlagenen assert
-Test ansehen:
def test_equality_fails():
i1 = Item("do something", "veit")
i2 = Item("do something else", "veit")
assert i1 == i2
Dieser Test schlägt fehl, aber interessant sind die Traceback-Informationen:
$ pytest tests/test_item_fails.py
============================= test session starts ==============================
…
collected 1 item
tests/test_item_fails.py F [100%]
=================================== FAILURES ===================================
_____________________________ test_equality_fails ______________________________
def test_equality_fails():
i1 = Item("do something", "veit")
i2 = Item("do something else", "veit.schiele")
> assert i1 == i2
E AssertionError: assert Item(summary=...odo', id=None) == Item(summary=...odo', id=None)
E
E Omitting 1 identical items, use -vv to show
E Differing attributes:
E ['summary', 'owner']
E
E Drill down into differing attribute summary:
E summary: 'do something' != 'do something else'...
E
E ...Full output truncated (8 lines hidden), use '-vv' to show
tests/test_item_fails.py:7: AssertionError
=========================== short test summary info ============================
FAILED tests/test_item_fails.py::test_equality_fails - AssertionError: assert Item(summary=...odo', id=None) == Item(summary=...od...
============================== 1 failed in 0.03s ===============================
Das sind eine Menge Informationen:
Für jeden fehlgeschlagenen Test wird die genaue Zeile des Fehlers mit einem
>
angezeigt, das auf den Fehler verweist.
Die E
-Zeilen zeigen Ihnen zusätzliche Informationen über den
assert
-Fehler, damit ihr herausfinden könnt, was falsch gelaufen ist.
Ich habe absichtlich zwei Fehlanpassungen in test_equality_fails()
eingegeben, aber nur die erste wurde angezeigt. Versuchen wir es noch einmal mit
der -vv
-Option, wie in der Fehlermeldung vorgeschlagen:
$ pytest -vv tests/test_item_fails.py
============================= test session starts ==============================
…
collected 1 item
tests/test_item_fails.py::test_equality_fails FAILED [100%]
=================================== FAILURES ===================================
_____________________________ test_equality_fails ______________________________
def test_equality_fails():
i1 = Item("do something", "veit")
i2 = Item("do something else", "veit.schiele")
> assert i1 == i2
E AssertionError: assert Item(summary='do something', owner='veit', state='todo', id=None) == Item(summary='do something else', owner='veit.schiele', state='todo', id=None)
E
E Matching attributes:
E ['state']
E Differing attributes:
E ['summary', 'owner']
E
E Drill down into differing attribute summary:
E summary: 'do something' != 'do something else'
E - do something else
E ? -----
E + do something
E
E Drill down into differing attribute owner:
E owner: 'veit' != 'veit.schiele'
E - veit.schiele
E + veit
tests/test_item_fails.py:7: AssertionError
=========================== short test summary info ============================
FAILED tests/test_item_fails.py::test_equality_fails - AssertionError: assert Item(summary='do something', owner='veit', state='to...
============================== 1 failed in 0.03s ===============================
pytest hat genau aufgelistet, welche Attribute übereinstimmen und welche nicht. Zudem wurden die genauen Abweichungen hervorgehoben.
Zum Vergleich können wir uns anzeigen lassen, was Python bei assert
-Fehlern
anzeigt. Um den Test direkt von Python aus aufrufen zu können, müssen wir einen Block am Ende von tests/test_item_fails.py
einfügen:
if __name__ == "__main__":
test_equality_fails()
Wenn wir den Test nun mit Python durchführen, erhalten wir folgendes Ergebnis:
python tests/test_item_fails.py
Traceback (most recent call last):
File "tests/test_item_fails.py", line 11, in <module>
test_equality_fails()
File "tests/test_item_fails.py", line 7, in test_equality_fails
assert i1 == i2
^^^^^^^^
AssertionError
Das sagt uns nicht viel. Die pytest-Ausgabe gibt uns viel mehr Informationen darüber, warum unsere Annahmen fehlgeschlagen sind.
Fehlschlagen mit pytest.fail()
und Exceptions#
Das Fehlschlagen von Behauptungen ist die Hauptursache dafür, dass Tests fehlgeschlagen. Aber das ist nicht der einzige Weg. Ein Test schlägt auch fehl, wenn es eine nicht abgefangene Exceptions gibt. Das kann passieren, wenn
eine
assert
-Anweisung fehlschlägt, was zu einerAssertionError
-Exception führt,der Testcode
pytest.fail()
aufruft, was zu einer Exception führt, odereine andere Exception ausgelöst wird.
Obwohl jede Exception einen Test fehlschlagen lassen kann, ziehe ich es vor,
assert
zu verwenden. In seltenen Fällen, in denen assert
nicht geeignet
ist, verwende ich meist pytest.fail()
.
Hier ist ein Beispiel für die Verwendung der Funktion fail()
von pytest, um
einen Test explizit fehlschlagen zu lassen:
def test_with_fail():
i1 = Item("do something", "veit")
i2 = Item("do something else", "veit.schiele")
if i1 != i2:
pytest.fail("The items are not identical!")
Die Ausgabe sieht wie folgt aus:
pytest tests/test_item_fails.py
============================= test session starts ==============================
…
collected 1 item
tests/test_item_fails.py F [100%]
=================================== FAILURES ===================================
________________________________ test_with_fail ________________________________
def test_with_fail():
i1 = Item("do something", "veit")
i2 = Item("do something else", "veit.schiele")
if i1 != i2:
> pytest.fail("The items are not identical!")
E Failed: The items are not identical!
tests/test_item_fails.py:10: Failed
=========================== short test summary info ============================
FAILED tests/test_item_fails.py::test_with_fail - Failed: The items are not identical!
============================== 1 failed in 0.03s ===============================
Beim Aufruf von pytest.fail()
oder dem Auslösen einer Exception, erhalten
wir nicht das von pytest angebotene assert
-Rewriting. Es gibt jedoch
sinnvolle Gelegenheiten, pytest.fail()
zu verwenden, wie z.B. in einem assertion
-Hilfsprogramm.
Schreiben von assertion
-Hilfsfunktionen#
Eine assertion
-Hilfsfunktion dient dazu, eine komplizierte
assertion
-Prüfung zu verpacken. Ein Beispiel: Die Datenklasse Item
ist so eingerichtet, dass zwei Items mit unterschiedlichen IDs trotzdem
Gleichheit berichten. Wenn wir eine strengere Prüfung wünschen, könnten wir eine
Hilfsfunktion namens assert_ident
wie folgt schreiben:
import pytest
from items import Item
def assert_ident(i1: Item, i2: Item):
__tracebackhide__ = True
assert i1 == i2
if i1.id != i2.id:
pytest.fail(f"The IDs do not match: {i1.id} != {i2.id}")
def test_ident():
i1 = Item("something to do", id=42)
i2 = Item("something to do", id=42)
assert_ident(i1, i2)
def test_ident_fail():
i1 = Item("something to do", id=42)
i2 = Item("something to do", id=43)
assert_ident(i1, i2)
Die assert_ident
-Funktion setzt __tracebackhide__ = True
. Die Folge ist,
dass fehlgeschlagene Tests nicht in den Traceback aufgenommen werden. Das
normale assert i1 == i2
wird dann verwendet, um alles außer id
auf
Gleichheit zu prüfen.
Schließlich werden die IDs überprüft pytest.fail()
verwendet, um den Test
mit einer hilfreichen Meldung fehlschlagen zu lassen. Schauen wir uns an, wie
das nach der Ausführung aussieht:
$ pytest tests/test_helper.py
============================= test session starts ==============================
…
collected 2 items
tests/test_helper.py .F [100%]
=================================== FAILURES ===================================
_______________________________ test_ident_fail ________________________________
def test_ident_fail():
i1 = Item("something to do", id=42)
i2 = Item("something to do", id=43)
> assert_ident(i1, i2)
E Failed: The IDs do not match: 42 != 43
tests/test_helper.py:22: Failed
=========================== short test summary info ============================
FAILED tests/test_helper.py::test_ident_fail - Failed: The IDs do not match: 42 != 43
========================= 1 failed, 1 passed in 0.03s ==========================
Testen auf erwartete Exceptions#
Wir haben uns angesehen, wie jede Exception einen Test zum Scheitern bringen
kann. Was aber, wenn ein Teil des Codes, den wir testen, eine Exception auslösen
soll? Hierfür verwenden wir pytest.raises()
, um auf erwartete Exceptions zu
testen. Ein Beispiel hierfür wäre die Items-API, die eine ItemsDB
-Klasse
hat, die ein Pfadargument benötigt.
from items.api import ItemsDB
def test_db_exists():
ItemsDB()
$ pytest --tb=short tests/test_db.py
============================= test session starts ==============================
…
collected 1 item
tests/test_db.py F [100%]
=================================== FAILURES ===================================
________________________________ test_db_exists ________________________________
tests/test_db.py:5: in test_db_exists
ItemsDB()
E TypeError: ItemsDB.__init__() missing 1 required positional argument: 'db_path'
=========================== short test summary info ============================
FAILED tests/test_db.py::test_db_exists - TypeError: ItemsDB.__init__() missing 1 required positional argument: 'db_p...
============================== 1 failed in 0.03s ===============================
Hier habe ich das kürzere Traceback-Format --tb=short
verwendet, weil wir
nicht den vollständigen Traceback sehen müssen, um herauszufinden, welche
Exception ausgelöst wurde.
Die Exception TypeError
erscheint sinnvoll, da der Fehler beim Versuch
auftritt, den benutzerdefinierten ItemsDB-Typ zu initialisieren. Wir können
einen Test schreiben, um sicherzustellen, dass diese Exception ausgelöst wird,
etwa so:
import pytest
from items.api import ItemsDB
def test_db_exists():
with pytest.raises(TypeError):
ItemsDB()
Die Anweisung with pytest.raises(TypeError):
besagt, dass der nächste
Codeblock eine TypeError
-Exception auslösen soll. Wenn keine Ausnahme
ausgelöst wird oder eine andere Ausnahme ausgelöst wird, schlägt der Test fehl.
Wir haben gerade in test_db_exists()
den Typ der Exception überprüft. Wir
können auch überprüfen, ob die Meldung korrekt ist, oder jeden anderen Aspekt
der Exception, wie z.B. zusätzliche Parameter:
def test_db_exists():
match_regex = "missing 1 .* positional argument"
with pytest.raises(TypeError, match=match_regex):
ItemsDB()
oder
def test_db_exists():
with pytest.raises(TypeError) as exc_info:
ItemsDB()
expected = "missing 1 required positional argument"
assert expected in str(exc_info.value)