Testsuite strukturieren¶
Stellt sicher, dass die Assertions am Ende von Testfunktionen aufbewahrt werden. Diese Empfehlung ist so verbreitet, dass sie mindestens zwei Namen hat:
- Arrange-Act-Assert (AAA)
wurde als Teil der testgetriebenen Entwicklung (TDD) populär.
- Given-When-Then (GWT)
wird im Kontext verhaltensgetriebener Entwicklung (BDD) verwendet.
Die Aufteilung in diese frei Phasen hat viele Vorteile. Dies trennt die Teile
- Given/Arrange
Der Ausgangszustand. Hier richtet ihr Daten oder die Umgebung ein, um die Aktion vorzubereiten.
- When/Act
Eine Aktion wird ausgeführt. Dies ist der Schwerpunkt des Tests – das Verhalten, von dem wir sicherstellen wollen, dass es richtig funktioniert.
- Then/Assert
Ein erwartetes Ergebnis oder ein Endzustand sollte eintreten. Am Ende des Tests stellen wir sicher, dass die Aktion zu dem erwarteten Verhalten geführt hat.
Ein häufig anzutreffendes Gegenmuster ist das Muster
Arrange–Assert–Act–Assert–Act–Assert…, bei dem eine Vielzahl von Aktionen,
gefolgt von Zustands- oder Verhaltensprüfungen, einen Arbeitsablauf validieren.
Dies erscheint vernünftig, bis der Test fehlschlägt. Jede der Aktionen könnte
den Fehler verursacht haben, so dass sich der Test nicht auf das Testen eines
bestimmten Verhaltens konzentriert. Oder es könnte die Einrichtung in Anordnen
gewesen sein, die den Fehler verursacht hat. Dieses verschachtelte
assert
-Muster führt zu Tests, die schwer zu debuggen und zu warten sind. Das
Festhalten an Given–When–Then oder Arrange–Act–Assert hält den Test
fokussiert und macht ihn wartungsfreundlicher.
Wenden wir diese Struktur als Beispiel auf einen unserer ersten Tests an:
def test_equality_fail():
# Given two item objects with known contents
i1 = Item("do something", "veit")
i2 = Item("do something else", "veit.schiele")
# WHEN the two item objects are not identical
if i1 != i2:
# THEN the result will be a string
pytest.fail("The items are not identical!")
Die Struktur hilft dabei, die Testfunktionen zu organisieren und sich auf das Testen eines Verhaltens zu konzentrieren. Die Struktur hilft euch auch dabei, an andere Testfälle zu denken. Die Konzentration auf einen Ausgangszustand hilft euch, an andere Zustände zu denken, die für das Testen der gleichen Aktion relevant sein könnten. Ebenso hilft die Konzentration auf ein ideales Ergebnis dabei, an andere mögliche Ergebnisse zu denken, wie z.B. Ausfallzustände oder Fehlerzustände, die ebenfalls mit anderen Testfällen getestet werden sollten.
Tests mit Klassen gruppieren¶
Bislang haben wir Testfunktionen innerhalb von Testmodulen in einem Dateisystemverzeichnis geschrieben. Diese Strukturierung des Testcodes funktioniert eigentlich ganz gut und ist für viele Projekte ausreichend. pytest erlaubt uns jedoch auch, Tests mit Klassen zu gruppieren. Nehmen wir einige der Testfunktionen, die sich auf die Gleichheit der Items beziehen, und gruppieren sie in einer Klasse:
class TestEquality:
def test_equality(self):
i1 = Item("do something", "veit", "todo", 42)
i2 = Item("do something", "veit", "todo", 42)
assert i1 == i2
def test_equality_with_diff_ids(self):
i1 = Item("do something", "veit", "todo", 42)
i2 = Item("do something", "veit", "todo", 43)
assert i1 == i2
def test_inequality(self):
i1 = Item("do something", "veit", "todo", 42)
i2 = Item("do something else", "veit", "done", 42)
assert i1 != i2
Der Code sieht so ziemlich genauso aus wie vorher, mit der Ausnahme, dass jede
Methode ein anfängliches self
-Argument haben muss. Wir können nun alle diese
Methoden zusammen ausführen, indem wir die Klasse angeben:
$ pytest -v tests/test_classes.py::TestEquality
============================= test session starts ==============================
…
collected 3 items
tests/test_classes.py::TestEquality::test_equality PASSED [ 33%]
tests/test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 66%]
tests/test_classes.py::TestEquality::test_inequality PASSED [100%]
============================== 3 passed in 0.00s ===============================
Wir können immer noch zu einer einzigen Methode kommen:
$ pytest -v tests/test_classes.py::TestEquality::test_equality
============================= test session starts ==============================
…
collected 1 item
tests/test_classes.py::TestEquality::test_equality PASSED [100%]
============================== 1 passed in 0.00s ===============================
Wenn ihr mit Objektorientierung und Klassenvererbung vertraut seid, könnt ihr Hierarchien von Testklassen für vererbte Hilfsmethoden verwenden. Ich empfehle euch, Testklassen auch in produktivem Testcode nur sparsam und hauptsächlich zur Gruppierung zu verwenden. Wenn ihr euch mit der Vererbung von Testklassen zu viel Mühe gebt, wird das zukünftig verwirrend werden.
Teilmenge von Tests ausführen¶
Im vorangegangenen Abschnitt haben wir Testklassen verwendet, um eine Teilmenge von Tests ausführen zu können. Die Ausführung einer kleinen Gruppe von Tests ist beim Debuggen sehr praktisch, oder wenn ihr die Tests auf einen bestimmten Abschnitt der Codebasis beschränken wollt, an dem ihr gerade arbeitet. pytest erlaubt euch, eine Teilmenge von Tests auf verschiedene Arten auszuführen:
Teilmenge |
Syntax |
---|---|
Alle Tests in einem Verzeichnis |
|
Alle Tests in einem Modul |
|
Alle Tests in einer Klasse |
|
Einzelne Testfunktion |
|
Einzelne Testmethode |
|
Tests, die einem Namensmuster entsprechen |
|
Tests nach Marker |
siehe Markers |
Ob pytest
euren Testcode findet, hängt von der Namensgebung ab:
Testdateien sollten
test_something.py
odersomething_test.py
.Testmethoden und Funktionen sollten
test_SOMETHING
genannt werden.Testklassen sollten den Namen
TestSomething
tragen.
Tipp
Verwendet eine Verzeichnisstruktur, die der Art und Weise entspricht, wie ihr euren Code ausführen möchtet, denn es ist einfach, ein komplettes Unterverzeichnis auszuführen. So könnt ihr Features und Funktionen unterteilen oder Subsysteme als Grundlage nehmen oder euch an der Code-Struktur orientieren.
Ihr könnt auch -k pattern
verwenden, um Verzeichnisse, Klassen oder
Testpräfixe zu filtern, also z.B. alle Tests der Klasse
TestEquality
$ pytest -v -k TestEquality
============================= test session starts ==============================
…
collected 7 items / 4 deselected / 3 selected
test_classes.py::TestEquality::test_equality PASSED [ 33%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 66%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
======================= 3 passed, 4 deselected in 0.00s ========================
oder alle Tests mit equality
im Namen:
pytest -v --tb=no -k equality
============================= test session starts ==============================
…
collected 7 items / 3 deselected / 4 selected
test_classes.py::TestEquality::test_equality PASSED [ 25%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 50%]
test_classes.py::TestEquality::test_inequality PASSED [ 75%]
test_item_fail.py::test_equality_fail FAILED [100%]
=========================== short test summary info ============================
FAILED test_item_fail.py::test_equality_fail - Failed: The items are not identical!
================== 1 failed, 3 passed, 3 deselected in 0.01s ===================
Eines davon ist leider unser Fehlerbeispiel. Wir können es beseitigen, indem wir den Ausdruck erweitern:
$ pytest -v --tb=no -k "equality and not equality_fail"
============================= test session starts ==============================
…
collected 7 items / 4 deselected / 3 selected
test_classes.py::TestEquality::test_equality PASSED [ 33%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 66%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
======================= 3 passed, 4 deselected in 0.00s ========================
Die Schlüsselwörter and
, not
, or
und ()
sind erlaubt, um
komplexe Ausdrücke zu erstellen. Hier ist ein Testlauf aller Tests mit oder „ids“ im Namen, aber nicht in der Klasse „TestEquality“:
$ pytest -v --tb=no -k "(inequality or id) and not _fail"
============================= test session starts ==============================
…
collected 7 items / 4 deselected / 3 selected
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 33%]
test_classes.py::TestEquality::test_inequality PASSED [ 66%]
test_helper.py::test_ident PASSED [100%]
======================= 3 passed, 4 deselected in 0.00s ========================
Die Keyword-Option -k
bietet zusammen mit and
, not
und or
eine
große Flexibilität bei der Auswahl der Tests, die ihr ausführen möchtet. Dies
erweist sich bei der Fehlersuche oder der Entwicklung neuer Tests als sehr
hilfreich.
Tipp
Es ist eine gute Idee, Anführungszeichen zu verwenden, wenn ihr einen Test zur Ausführung auswählt, da die Bindestriche, Klammern und Leerzeichen die Shells durcheinander bringen können.