Pytest-Grundlagen

Abgeschlossen

Beginnen wir mit dem Testen mit Pytest. Wie wir in der vorherigen Einheit erwähnt haben, ist Pytest sehr konfigurierbar und kann komplexe Testsuiten verarbeiten. Das Framework erfordert jedoch nicht viel Konfiguration vorab, um mit dem Schreiben von Tests zu beginnen. Je einfacher ein Framework Ihnen das Schreiben von Tests macht, desto besser.

Am Ende dieses Abschnitts sollten Sie alle Informationen haben, die Sie zum Schreiben Ihrer ersten Tests benötigen, und um sie mit Pytest auszuführen.

Konventionen

Bevor wir uns mit dem Schreiben von Tests befassen, müssen wir einige der Testkonventionen abdecken, auf die Pytest basiert.

Es gibt keine strengen Regeln für Testdateien, Testverzeichnisse oder allgemeine Testlayouts in Python. Wenn Sie diese Regeln kennen, können Sie die automatische Testermittlung sowie die -ausführung nutzen, ohne dass eine zusätzliche Konfiguration erforderlich ist.

Testverzeichnis und Testdateien

Das Hauptverzeichnis für Tests ist das Testverzeichnis (tests). Sie können dieses Verzeichnis auf der Stammebene des Projekts platzieren, aber es ist auch nicht ungewöhnlich, wenn es neben Codemodulen existiert.

Hinweis

In diesem Modul verwenden wir standardmäßig Tests im Stammverzeichnis eines Projekts.

Sehen wir uns an, wie der Stamm eines kleinen Python-Projekts namens jformat aussieht:

.
├── README.md
├── jformat
│   ├── __init__.py
│   └── main.py
├── setup.py
└── tests
    └── test_main.py

Das Testverzeichnis befindet sich im Stammverzeichnis des Projekts mit einer einzigen Testdatei. In diesem Fall wird die Testdatei test_main.py aufgerufen. In diesem Beispiel werden zwei wichtige Konventionen veranschaulicht:

  • Verwenden eines Testverzeichnisses zum Platzieren von Testdateien und geschachtelten Testverzeichnissen.
  • Fügen Sie das Präfix test zu Testdateien hinzu. Das Präfix gibt an, dass die Datei Testcode enthält.

Achtung

Vermeiden Sie die Verwendung von test (Singularform) als Verzeichnisname. Der Name test ist ein Python-Modul, sodass das Erstellen eines Verzeichnisses mit demselben Namen es außer Kraft setzen würde. Verwenden Sie stattdessen immer den Plural tests.

Testfunktionen

Eines der starken Argumente für die Verwendung von Pytest besteht darin, dass Sie Testfunktionen schreiben können. Ähnlich wie Testdateien muss Testfunktionen test_ vorangestellt werden. Das test_-Präfix stellt sicher, dass Pytest den Test erfasst und ausführt.

So sieht eine einfache Testfunktion aus:

def test_main():
    assert "a string value" == "a string value"

Hinweis

Wenn Sie mit unittest vertraut sind, kann es überraschend sein, dass assert in der Testfunktion verwendet wird. Wir werden schlichte Assertionen später noch ausführlich erläutern. Mit Pytest erhalten Sie jedoch mit schlichten Assertionen eine ausführliche Fehlerberichterstattung.

Testklassen und Testmethoden

Ähnlich wie bei den Konventionen für Dateien und Funktionen verwenden Testklassen und Testmethoden die folgenden Konventionen:

  • Testklassen erhalten das Präfix Test.
  • Testmethoden erhalten das Präfix test_.

Ein wichtiger Unterschied bei der Python-Bibliothek unittest besteht darin, dass keine Vererbung erforderlich ist.

Im folgenden Beispiel werden diese Präfixe und andere Python-Benennungskonventionen für Klassen und Methoden verwendet. Es zeigt eine kleine Testklasse, die Benutzernamen in einer Anwendung überprüft.

class TestUser:

    def test_username(self):
        assert default() == "default username"

Tests durchführen

Pytest ist sowohl ein Testframework als auch ein Testprogramm. Das Testprogramm ist eine ausführbare Datei in der Befehlszeile, die auf hoher Ebene folgende Aufgaben ausführen kann:

  • Führen Sie die Sammlung von Tests durch Suchen nach Testdateien, Testklassen und Testfunktionen für einen Testlauf durch.
  • Initiieren Sie einen Testlauf durch Ausführen aller Tests.
  • Verfolgen Sie Fehler und das Bestehen von Tests nach.
  • Stelle Sie eine umfassende Berichterstellung am Ende eines Testlaufs bereit.

Hinweis

Da Pytest eine externe Bibliothek ist, muss sie installiert werden, damit sie verwendet werden kann.

Anhand dieser Inhalte in einer test_main.py-Datei können wir sehen, wie Pytest sich verhält, wenn die Tests ausgeführt werden:

# contents of test_main.py file

def test_main():
    assert True

In der Befehlszeile können wir die ausführbare Datei pytest in demselben Pfad ausführen, in dem die Datei test_main.py vorhanden ist:

 $ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item

test_main.py .                                                       [100%]

============================ 1 passed in 0.00s =============================

Hinter den Kulissen hat Pytest den Beispieltest in der Testdatei gesammelt, ohne dass eine Konfiguration erforderlich ist.

Die leistungsstarke Assert-Anweisung

Bisher verwenden unsere Testbeispiele den einfachen assert-Aufruf. In Python wird die assert-Anweisung in der Regel nicht für Tests verwendet, da es keine richtige Berichterstattung gibt, wenn die Assertion fehlschlägt. Pytest hat diese Einschränkung jedoch nicht. Hinter den Kulissen ermöglicht es Pytest der Anweisung, umfangreiche Vergleiche auszuführen, ohne den Benutzer bzw. die Benutzerin zu zwingen, mehr Code zu schreiben oder etwas zu konfigurieren.

Mithilfe der einfachen assert-Anweisung können Sie die Operatoren von Python verwenden. Zum Beispiel: >, <, !=, >= oder <=. Alle Python-Operatoren sind gültig. Diese Funktion ist möglicherweise das wichtigste Feature von Pytest: Sie müssen keine neue Syntax zum Schreiben von Assertionen erlernen.

Sehen wir uns an, wie dies beim Umgang mit gängigen Vergleichen mit Python-Objekten angewendet wird. In diesem Fall sehen wir den Fehlerbericht durch, wenn wir lange Zeichenfolgen vergleichen:

================================= FAILURES =================================
____________________________ test_long_strings _____________________________

    def test_long_strings():
        left = "this is a very long strings to be compared with another long string"
        right = "This is a very long string to be compared with another long string"
>       assert left == right
E       AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E         - This is a very long string to be compared with another long string
E         ? ^
E         + this is a very long strings to be compared with another long string
E         ? ^                         +

test_main.py:4: AssertionError

Pytest zeigt nützlichen Kontext um den Fehler herum. Falsche Großschreibung am Anfang der Zeichenfolge und ein zusätzliches Zeichen in einem Wort. Pytest kann aber auch über Zeichenfolgen hinaus bei anderen Objekten und Datenstrukturen helfen. Hier erfahren Sie beispielsweise, wie es sich mit Listen verhält:

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E         At index 1 diff: 'wheat' != 'coffee'
E         Full diff:
E         - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E         ?                     ---------
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?           +++++++++

test_main.py:9: AssertionError

Dieser Bericht gibt an, dass der Index 1 (zweites Element in der Liste) anders ist. Es wird nicht nur die Indexnummer identifiziert, Pytest stellt auch eine Darstellung der Fehler bereit. Abgesehen von Elementvergleichen kann Pytest auch melden, wenn Elemente fehlen, und Informationen bereitstellen, die genau beschreiben, um welches Element es sich möglicherweise handeln könnte. Im folgenden Fall ist dies "milk":

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E         At index 2 diff: 'coffee' != 'salt'
E         Left contains one more item: 'milk'
E         Full diff:
E         - ['sugar', 'wheat', 'salt', 'water', 'milk']
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?                    ++++++++++

test_main.py:9: AssertionError

Zuletzt sehen wir uns an, wie sich Pytest mit Wörterbüchern verhält. Der Vergleich von zwei großen Wörterbüchern kann überwältigend sein, wenn Fehler auftreten. Pytest stellt jedoch den Kontext bereit und zeigt den Fehler auf:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         {'number': 39} != {'number': 38}
E         Full diff:
E           {
E            'county': 'Frett',...
E
E         ...Full output truncated (12 lines hidden), use '-vv' to show

In diesem Test sind zwei Fehler im Wörterbuch vorhanden. Ein Fehler ist, dass der "street"-Wert unterschiedlich ist. Der zweite Fehler ist, dass "number" nicht übereinstimmt.

Pytest erkennt diese Unterschiede genau (auch wenn es sich um einen Fehler in einem einzigen Test handelt). Da die Wörterbücher viele Elemente enthalten, lässt Pytest die identischen Teile aus und zeigt nur relevante Inhalte an. Sehen wir uns an, was passiert, wenn wir das vorgeschlagene -vv-Flag verwenden, um die Ausführlichkeit in der Ausgabe zu erhöhen:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E         Common items:
E         {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E         Differing items:
E         {'number': 39} != {'number': 38}
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         Full diff:
E           {
E            'county': 'Frett',
E         -  'number': 38,
E         ?             ^
E         +  'number': 39,
E         ?             ^
E            'state': 'Nevada',
E         -  'street': 'Ferry Lane',
E         ?                    - ^
E         +  'street': 'Ferry Ln.',
E         ?                     ^
E            'zipcode': 30877,
E           }

Durch die Ausführung von pytest -vv erhöht die Berichterstellung die Detailmenge und stellt einen präzisen Vergleich bereit. Dieser Bericht erkennt und zeigt nicht nur den Fehler an, sondern ermöglicht es Ihnen, schnell Änderungen vorzunehmen, um das Problem zu beheben.