pytest-2.3: Begründung für die Evolution von Fixtures/Funcargs

Zielgruppe: Zum Lesen dieses Dokuments sind Grundkenntnisse über Python-Tests, xUnit-Setup-Methoden und den (früheren) grundlegenden pytest-Funcarg-Mechanismus erforderlich. Siehe Funcargs und pytest_funcarg__. Wenn Sie neu bei pytest sind, können Sie diesen Abschnitt einfach ignorieren und die anderen Abschnitte lesen.

Mängel des früheren pytest_funcarg__ Mechanismus

Der Funcarg-Mechanismus vor pytest-2.3 ruft jedes Mal eine Factory auf, wenn ein Funcarg für eine Testfunktion benötigt wird. Wenn eine Factory eine Ressource über verschiedene Scopes hinweg wiederverwenden möchte, wurde oft der request.cached_setup()-Helfer verwendet, um das Caching von Ressourcen zu verwalten. Hier ist ein einfaches Beispiel, wie wir ein datenbankspezifisches Objekt für die gesamte Sitzung implementieren könnten.

# content of conftest.py
class Database:
    def __init__(self):
        print("database instance created")

    def destroy(self):
        print("database instance destroyed")


def pytest_funcarg__db(request):
    return request.cached_setup(
        setup=DataBase, teardown=lambda db: db.destroy, scope="session"
    )

Dieser Ansatz hat mehrere Einschränkungen und Schwierigkeiten

  1. Das Scoping der Funcarg-Ressourcenerstellung ist nicht einfach, stattdessen muss man die komplizierten Funktionsweisen von cached_setup() verstehen.

  2. Die Parametrisierung der "db"-Ressource ist nicht einfach: Sie müssen einen "parametrize"-Decorator anwenden oder einen pytest_generate_tests Hook implementieren, der parametrize() aufruft, was die Parametrisierung an den Stellen durchführt, an denen die Ressource verwendet wird. Außerdem müssen Sie die Factory so modifizieren, dass sie einen extrakey-Parameter verwendet, der request.param an den Request.cached_setup-Aufruf übergibt.

  3. Mehrere parametrisierte Ressourcen auf Sitzungsebene sind gleichzeitig aktiv, was es schwierig macht, den globalen Zustand der zu testenden Anwendung zu beeinflussen.

  4. Es gibt keine Möglichkeit, Funcarg-Factories in xUnit-Setup-Methoden zu verwenden.

  5. Eine nicht-parametrisierte Fixture-Funktion kann keine parametrisierte Funcarg-Ressource verwenden, wenn sie nicht in der Signatur der Testfunktion angegeben ist.

All diese Einschränkungen werden mit pytest-2.3 und seinem verbesserten Fixture-Mechanismus behoben.

Direktes Scoping von Fixture/Funcarg-Factories

Anstatt cached_setup() mit einem Cache-Scope aufzurufen, können Sie den @pytest.fixture-Decorator verwenden und direkt den Scope angeben.

@pytest.fixture(scope="session")
def db(request):
    # factory will only be invoked once per session -
    db = DataBase()
    request.addfinalizer(db.destroy)  # destroy when session is finished
    return db

Diese Factory-Implementierung muss cached_setup() nicht mehr aufrufen, da sie nur einmal pro Sitzung aufgerufen wird. Außerdem registriert request.addfinalizer() einen Finalizer gemäß dem angegebenen Ressourcenscope, auf dem die Factory-Funktion operiert.

Direkte Parametrisierung von Funcarg-Ressourcen-Factories

Zuvor konnten Funcarg-Factories keine direkte Parametrisierung bewirken. Sie mussten einen @parametrize-Decorator auf Ihrer Testfunktion angeben oder einen pytest_generate_tests Hook implementieren, um die Parametrisierung durchzuführen, d.h. einen Test mehrmals mit unterschiedlichen Wertesätzen aufzurufen. pytest-2.3 führt einen Decorator für die Factory selbst ein.

@pytest.fixture(params=["mysql", "pg"])
def db(request): ...  # use request.param

Hier wird die Factory zweimal aufgerufen (mit den jeweiligen "mysql"- und "pg"-Werten als request.param-Attributen) und alle Tests, die "db" benötigen, werden ebenfalls zweimal ausgeführt. Die Werte "mysql" und "pg" werden auch für die Berichterstattung der Test-Invokationsvarianten verwendet.

Diese neue Art der Parametrisierung von Funcarg-Factories sollte in vielen Fällen die Wiederverwendung bereits geschriebener Factories ermöglichen, da effektiv request.param bereits verwendet wurde, als Testfunktionen/Klassen über metafunc.parametrize(indirect=True) aufgerufen wurden.

Natürlich ist es vollkommen in Ordnung, Parametrisierung und Scoping zu kombinieren.

@pytest.fixture(scope="session", params=["mysql", "pg"])
def db(request):
    if request.param == "mysql":
        db = MySQL()
    elif request.param == "pg":
        db = PG()
    request.addfinalizer(db.destroy)  # destroy when session is finished
    return db

Dies würde alle Tests, die die pro Sitzung benötigte "db"-Ressource verwenden, zweimal ausführen und dabei die von den beiden jeweiligen Aufrufen der Factory-Funktion erzeugten Werte erhalten.

Kein pytest_funcarg__ Präfix bei Verwendung des @fixture Decorators

Bei Verwendung des @fixture-Decorators bezeichnet der Name der Funktion den Namen, unter dem die Ressource als Funktionsargument abgerufen werden kann.

@pytest.fixture()
def db(request): ...

Der Name, unter dem die Funcarg-Ressource angefordert werden kann, ist db.

Sie können immer noch die "alte" Nicht-Decorator-Methode zur Angabe von Funcarg-Factories verwenden, also

def pytest_funcarg__db(request): ...

Aber es ist dann nicht möglich, Scoping und Parametrisierung zu definieren. Daher wird die Verwendung des Factory-Decorators empfohlen.

Behebung von Sitzungs-Setup / Autouse-Fixtures

pytest bot lange Zeit einen pytest_configure und einen pytest_sessionstart Hook an, die oft zum Einrichten globaler Ressourcen verwendet wurden. Dies hat mehrere Probleme

  1. Beim verteilten Testen würde der verwaltende Prozess Testressourcen einrichten, die nie benötigt werden, da er nur die Aktivitäten der Worker-Prozesse koordiniert.

  2. Wenn Sie nur eine Sammlung durchführen (mit "--collect-only"), wird die Ressourcenerstellung trotzdem ausgeführt.

  3. Wenn ein pytest_sessionstart in einer Unterverzeichnis-conftest.py-Datei enthalten ist, wird es nicht aufgerufen. Dies liegt daran, dass dieser Hook tatsächlich für die Berichterstattung verwendet wird, insbesondere für den Test-Header mit Plattform-/benutzerdefinierten Informationen.

Darüber hinaus war es nicht einfach, ein skopiertes Setup von Plugins oder conftest-Dateien zu definieren, außer durch die Implementierung eines pytest_runtest_setup() Hooks und die eigene Verwaltung von Scoping/Caching. Und dies mit Parametrisierung zu tun ist praktisch unmöglich, da pytest_runtest_setup() während der Testausführung aufgerufen wird und die Parametrisierung zur Sammlungszeit erfolgt.

Folglich sind pytest_configure/session/runtest_setup oft nicht für die Implementierung gängiger Fixture-Bedürfnisse geeignet. Daher führt pytest-2.3 Autouse-Fixtures (Fixtures, die Sie nicht explizit anfordern müssen) ein, die vollständig in den generischen Fixture-Mechanismus integriert sind und viele frühere Verwendungen von pytest-Hooks obsolet machen.

Funcarg-/Fixture-Erkennung erfolgt nun zur Sammlungszeit

Seit pytest-2.3 wird die Erkennung von Fixture/Funcarg-Factories zur Sammlungszeit durchgeführt. Dies ist insbesondere für große Test-Suiten effizienter. Darüber hinaus sollte ein Aufruf von "--collect-only" in der Zukunft in der Lage sein, viele Setup-Informationen anzuzeigen und somit eine gute Methode darzustellen, um einen Überblick über das Fixture-Management in Ihrem Projekt zu erhalten.

Fazit und Kompatibilitätshinweise

Funcargs wurden ursprünglich mit pytest-2.0 eingeführt. In pytest-2.3 wurde der Mechanismus erweitert und verfeinert und wird nun als Fixtures bezeichnet.

  • Zuvor wurden Funcarg-Factories mit einem speziellen pytest_funcarg__NAME-Präfix anstelle des @pytest.fixture-Decorators angegeben.

  • Factories erhielten ein request-Objekt, das das Caching über request.cached_setup()-Aufrufe verwaltete und die Verwendung anderer Funcargs über request.getfuncargvalue()-Aufrufe ermöglichte. Diese komplizierten APIs erschwerten eine korrekte Parametrisierung und die Implementierung von Ressourcenspeicher. Der neue pytest.fixture()-Decorator ermöglicht die Deklaration des Scopes und überlässt pytest die Details.

  • Wenn Sie Parametrisierung und Funcarg-Factories verwendet haben, die request.cached_setup() nutzten, wird empfohlen, ein paar Minuten zu investieren und Ihren Fixture-Funktionscode zu vereinfachen, um stattdessen den Fixture-Referenz-Decorator zu verwenden. Dies ermöglicht auch die automatische Gruppierung von Tests pro Ressource.