Testklassen und Methoden

Abgeschlossen

Neben dem Schreiben von Testfunktionen ermöglicht Pytest die Verwendung von Klassen. Wie bereits erwähnt, ist keine Vererbung erforderlich, und die Testklassen folgen einigen einfachen Regeln. Die Verwendung von Klassen bietet Ihnen mehr Flexibilität und Wiederverwendbarkeit. Danach verhindert Pytest, dass Sie Tests auf eine bestimmte Weise schreiben müssen.

Genau wie bei Funktionen können Sie mit der assert-Anweisung weiterhin Assertionen schreiben.

Erstellen einer Testklasse

Verwenden wir ein reelles Szenario, um zu sehen, wie Testklassen helfen können. Die folgende Funktion überprüft, ob eine bestimmte Datei „ja“ in ihrem Inhalt enthält. Wenn ja, gibt sie True zurück. Wenn die Datei nicht vorhanden ist oder wenn sie in ihrem Inhalt „nein“ enthält, gibt sie False zurück. Dieses Szenario ist bei asynchronen Aufgaben üblich, die das Dateisystem verwenden, um die Fertigstellung anzugeben.

So sieht die Funktion aus:

import os

def is_done(path):
    if not os.path.exists(path):
        return False
    with open(path) as _f:
        contents = _f.read()
    if "yes" in contents.lower():
        return True
    elif "no" in contents.lower():
        return False

Nun sehen Sie, wie eine Klasse mit zwei Tests (einer für jede Bedingung) in einer Datei mit dem Namen test_files.py aussieht:

class TestIsDone:

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Achtung

Die Testmethoden verwenden den Pfad "/tmp " für eine temporäre Testdatei, da sie für das Beispiel einfacher zu verwenden ist. Wenn Sie jedoch temporäre Dateien verwenden müssen, sollten Sie eine Bibliothek wie tempfile verwenden, die diese sicher für Sie erstellen (und entfernen) kann. Nicht jedes System verfügt über ein /tmp-Verzeichnis , und dieser Speicherort ist je nach Betriebssystem möglicherweise nicht temporär.

Wenn Sie die Tests mit dem -v-Flag ausführen, um die Ausführlichkeit zu erhöhen, werden die Tests als bestanden angezeigt:

pytest -v test_files.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
cachedir: .pytest_cache
rootdir: /private/
collected 2 items

test_files.py::TestIsDone::test_yes PASSED                               [ 50%]
test_files.py::TestIsDone::test_no PASSED                                [100%]

============================== 2 passed in 0.00s ===============================

Obwohl die Tests bestanden werden, sehen sie aus, als würden sie sich wiederholen. Außerdem hinterlassen sie Dateien, nachdem der Test abgeschlossen wurde. Bevor wir sehen, wie wir dies verbessern können, behandeln wir im nächsten Abschnitt die Hilfsmethoden.

Hilfsmethoden

In einer Testklasse gibt es einige Methoden, mit denen Sie die Testausführung einrichten und abreißen können. Pytest führt sie automatisch aus, wenn sie definiert sind. Um diese Methoden zu verwenden, sollten Sie wissen, dass sie eine bestimmte Reihenfolge und ein bestimmtes Verhalten aufweisen.

  • setup: Führt einmal vor jedem Test in einer Klasse aus.
  • teardown: Wird einmal nach jedem Test in einer Klasse ausgeführt.
  • setup_class: Wird einmal vor allen Tests in einer Klasse ausgeführt.
  • teardown_class: Wird nach allen Tests in einer Klasse einmal ausgeführt.

Wenn Tests ähnliche (oder identische) Ressourcen zum Funktionieren benötigen, ist es hilfreich, Setupmethoden zu schreiben. Im Idealfall sollte ein Test keine Ressourcen hinterlassen, wenn er abgeschlossen ist. Methoden zum Beenden können bei der Testbereinigung in diesen Situationen helfen.

Bereinigen

Lassen Sie uns eine aktualisierte Testklasse überprüfen, welche die Dateien nach jedem Test bereinigt:

class TestIsDone:

    def teardown(self):
        if os.path.exists("/tmp/test_file"):
            os.remove("/tmp/test_file")

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Da wir die teardown()-Methode verwendet haben, lässt diese Testklasse keine /tmp/test_file mehr zurück.

Einrichtung

Eine weitere Verbesserung, die wir an dieser Klasse vornehmen können, ist das Hinzufügen einer Variablen, die auf die Datei verweist. Da die Datei jetzt an sechs Stellen deklariert wird, würde jede Änderung des Pfads dazu führen, dass er an all diesen Stellen geändert werden muss. Dieses Beispiel zeigt die Klasse mit einer hinzugefügten setup()-Methode, welche die Pfadvariable deklariert:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def test_yes(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("no")
        assert is_done(self.tmp_file) is False

Benutzerdefinierte Hilfsmethoden

Sie können benutzerdefinierte Hilfsmethoden in einer Klasse erstellen. Diese Methoden dürfen nicht test als Präfix erhalten und können nicht als Einrichtungs- oder Bereinigungsmethoden benannt werden. In der TestIsDone Klasse konnten wir das Erstellen der temporären Datei in einem benutzerdefinierten Hilfsprogramm automatisieren. Diese benutzerdefinierte Hilfsmethode könnte wie in diesem Beispiel aussehen:

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

Die write_tmp_file()-Methode wird von Pytest nicht automatisch ausgeführt. Zudem können andere Methoden sie direkt aufrufen, um bei sich wiederholenden Aufgaben wie dem Schreiben in eine Datei Zeit zu sparen.

Die gesamte Klasse sieht nach dem Aktualisieren der Testmethoden wie dieses Beispiel aus, damit diese das benutzerdefinierte Hilfsprogramm verwendet:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

    def test_yes(self):
        self.write_tmp_file("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        self.write_tmp_file("no")
        assert is_done(self.tmp_file) is False

Wenn eine Klasse anstelle einer Funktion verwendet werden soll

Es gibt keine strengen Regeln dafür, wann eine Klasse anstelle einer Funktion verwendet werden soll. Es empfiehlt sich immer, die Konventionen in aktuellen Projekten und Teams zu befolgen, mit denen Sie arbeiten. Im Folgenden finden Sie einige allgemeine Fragen, die Ihnen dabei helfen können, zu bestimmen, wann sie eine Klasse verwenden:

  • Benötigen Ihre Tests ähnlichen Setup- oder Bereinigungshilfscode?
  • Macht das Gruppieren Ihrer Tests logisch Sinn?
  • Gibt es mindestens ein paar Tests in Ihrer Testsuite?
  • Könnten Ihre Tests von einer gemeinsamen Reihe von Hilfsfunktionen profitieren?