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

pytest path

Alle Tests in einem Modul

pytest path/test_module.py

Alle Tests in einer Klasse

pytest path/test_module.py::TestClass

Einzelne Testfunktion

pytest path/test_module.py::test_function

Einzelne Testmethode

pytest path/test_module.py::TestClass::test_method

Tests, die einem Namensmuster entsprechen

pytest -k pattern

Tests nach Marker

siehe Markers

Ob pytest euren Testcode findet, hängt von der Namensgebung ab:

  • Testdateien sollten test_something.py oder something_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.