Wie man unittest-basierte Tests mit pytest verwendet

pytest unterstützt das Ausführen von Python unittest-basierten Tests sofort. Es ist dazu gedacht, bestehende unittest-basierte Test-Suiten zu nutzen, um pytest als Test-Runner zu verwenden und auch die schrittweise Anpassung der Test-Suite zu ermöglichen, um die Funktionen von pytest vollständig zu nutzen.

Um eine bestehende Test-Suite im unittest-Stil mit pytest auszuführen, tippen Sie

pytest tests

pytest sammelt automatisch unittest.TestCase-Unterklassen und ihre test-Methoden in Dateien test_*.py oder *_test.py.

Fast alle unittest-Funktionen werden unterstützt

Bis zu diesem Zeitpunkt hat pytest keine Unterstützung für die folgenden Funktionen

Vorteile sofort

Durch die Ausführung Ihrer Test-Suite mit pytest können Sie mehrere Funktionen nutzen, in den meisten Fällen ohne bestehenden Code ändern zu müssen.

pytest-Funktionen in unittest.TestCase-Unterklassen

Die folgenden pytest-Funktionen funktionieren in unittest.TestCase-Unterklassen

Die folgenden pytest-Funktionen funktionieren **nicht** und werden aufgrund unterschiedlicher Designphilosophien wahrscheinlich auch nie funktionieren.

Drittanbieter-Plugins funktionieren möglicherweise gut oder schlecht, abhängig vom Plugin und der Test-Suite.

Mischen von pytest-Fixtures in unittest.TestCase-Unterklassen mithilfe von Markierungen

Das Ausführen Ihrer Unittests mit pytest ermöglicht die Verwendung seines Fixture-Mechanismus mit Tests im unittest.TestCase-Stil. Vorausgesetzt, Sie haben zumindest die pytest-Fixture-Funktionen überflogen, beginnen wir gleich mit einem Beispiel, das eine pytest db_class-Fixture integriert, ein klassenspeicherndes Datenbankobjekt einrichtet und es dann aus einem Unittest-artigen Test referenziert.

# content of conftest.py

# we define a fixture function below and it will be "used" by
# referencing its name from tests

import pytest


@pytest.fixture(scope="class")
def db_class(request):
    class DummyDB:
        pass

    # set a class attribute on the invoking test context
    request.cls.db = DummyDB()

Dies definiert eine Fixture-Funktion db_class, die - wenn sie verwendet wird - einmal für jede Testklasse aufgerufen wird und das Klassenattribut db auf eine Instanz von DummyDB setzt. Die Fixture-Funktion erreicht dies, indem sie ein spezielles request-Objekt empfängt, das Zugriff auf den anfordernden Testkontext wie das Attribut cls bietet, was die Klasse bezeichnet, aus der die Fixture verwendet wird. Diese Architektur entkoppelt das Schreiben von Fixtures vom eigentlichen Testcode und ermöglicht die Wiederverwendung der Fixture durch eine minimale Referenz, den Namen der Fixture. Schreiben wir also eine tatsächliche unittest.TestCase-Klasse, die unsere Fixture-Definition verwendet.

# content of test_unittest_db.py

import unittest

import pytest


@pytest.mark.usefixtures("db_class")
class MyTest(unittest.TestCase):
    def test_method1(self):
        assert hasattr(self, "db")
        assert 0, self.db  # fail for demo purposes

    def test_method2(self):
        assert 0, self.db  # fail for demo purposes

Der Klassen-Decorator @pytest.mark.usefixtures("db_class") stellt sicher, dass die pytest-Fixture-Funktion db_class einmal pro Klasse aufgerufen wird. Aufgrund der bewusst fehlerhaften Assert-Anweisungen können wir die self.db-Werte im Traceback betrachten.

$ pytest test_unittest_db.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_unittest_db.py FF                                               [100%]

================================= FAILURES =================================
___________________________ MyTest.test_method1 ____________________________

self = <test_unittest_db.MyTest testMethod=test_method1>

    def test_method1(self):
        assert hasattr(self, "db")
>       assert 0, self.db  # fail for demo purposes
        ^^^^^^^^^^^^^^^^^
E       AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E       assert 0

test_unittest_db.py:11: AssertionError
___________________________ MyTest.test_method2 ____________________________

self = <test_unittest_db.MyTest testMethod=test_method2>

    def test_method2(self):
>       assert 0, self.db  # fail for demo purposes
        ^^^^^^^^^^^^^^^^^
E       AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E       assert 0

test_unittest_db.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
============================ 2 failed in 0.12s =============================

Dieser standardmäßige pytest-Traceback zeigt, dass sich die beiden Testmethoden dieselbe self.db-Instanz teilen, was unsere Absicht war, als wir die klassenbezogene Fixture-Funktion oben geschrieben haben.

Verwendung von Autouse-Fixtures und Zugriff auf andere Fixtures

Obwohl es normalerweise besser ist, die Nutzung benötigter Fixtures für einen bestimmten Test explizit zu deklarieren, möchten Sie möglicherweise manchmal Fixtures haben, die in einem bestimmten Kontext automatisch verwendet werden. Schließlich erzwingt der traditionelle Stil von unittest-Setup die Verwendung dieser impliziten Fixture-Schreibweise, und es besteht die Chance, dass Sie daran gewöhnt sind oder sie mögen.

Sie können Fixture-Funktionen mit @pytest.fixture(autouse=True) kennzeichnen und die Fixture-Funktion in dem Kontext definieren, in dem sie verwendet werden soll. Betrachten wir eine initdir-Fixture, die dafür sorgt, dass alle Testmethoden einer TestCase-Klasse in einem temporären Verzeichnis mit einer vorinitialisierten samplefile.ini ausgeführt werden. Unsere initdir-Fixture verwendet selbst die integrierte pytest-Fixture tmp_path, um die Erstellung eines pro Test temporären Verzeichnisses zu delegieren.

# content of test_unittest_cleandir.py
import unittest

import pytest


class MyTest(unittest.TestCase):
    @pytest.fixture(autouse=True)
    def initdir(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)  # change to pytest-provided temporary directory
        tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")

    def test_method(self):
        with open("samplefile.ini", encoding="utf-8") as f:
            s = f.read()
        assert "testdata" in s

Aufgrund des autouse-Flags wird die initdir-Fixture-Funktion für alle Methoden der Klasse verwendet, in der sie definiert ist. Dies ist eine Abkürzung für die Verwendung einer @pytest.mark.usefixtures("initdir")-Markierung auf der Klasse, wie im vorherigen Beispiel.

Ausführen dieses Testmoduls …

$ pytest -q test_unittest_cleandir.py
.                                                                    [100%]
1 passed in 0.12s

… ergibt einen bestandenen Test, da die initdir-Fixture-Funktion vor der test_method ausgeführt wurde.

Hinweis

unittest.TestCase-Methoden können keine Fixture-Argumente direkt empfangen, da deren Implementierung wahrscheinlich die Fähigkeit beeinträchtigt, allgemeine unittest.TestCase-Test-Suiten auszuführen.

Die obigen Beispiele für usefixtures und autouse sollten helfen, pytest-Fixtures in Unittest-Suiten zu integrieren.

Sie können auch schrittweise von der Unterklasse von unittest.TestCase zu *einfachen asserts* übergehen und dann Schritt für Schritt die volle Funktionsvielfalt von pytest nutzen.

Hinweis

Aufgrund architektonischer Unterschiede zwischen den beiden Frameworks wird das Setup und Teardown für unittest-basierte Tests während der call-Phase des Testens durchgeführt, anstatt in den standardmäßigen setup- und teardown-Stufen von pytest. Dies kann in einigen Situationen wichtig sein zu verstehen, insbesondere bei der Fehlersuche. Wenn beispielsweise eine unittest-basierte Suite während des Setups Fehler aufweist, meldet pytest während seiner setup-Phase keine Fehler und löst den Fehler stattdessen während des call aus.