Plugins schreiben

Es ist einfach, lokale conftest Plugins für Ihr eigenes Projekt zu implementieren oder pip-installierbare Plugins, die in vielen Projekten, einschließlich Drittanbieterprojekten, verwendet werden können. Bitte beachten Sie Plugins installieren und verwenden, wenn Sie nur Plugins verwenden, aber nicht schreiben möchten.

Ein Plugin enthält eine oder mehrere Hook-Funktionen. Hooks schreiben erklärt die Grundlagen und Details, wie Sie selbst eine Hook-Funktion schreiben können. pytest implementiert alle Aspekte der Konfiguration, Sammlung, Ausführung und Berichterstattung, indem es gut spezifizierte Hooks der folgenden Plugins aufruft

  • eingebaute Plugins: geladen aus Pytests internem _pytest Verzeichnis.

  • Externe Plugins: installierte Drittanbieter-Module, die über Entry Points in ihren Paketierungsmetadaten entdeckt werden.

  • conftest.py Plugins: Module, die automatisch in Testverzeichnissen entdeckt werden.

Im Prinzip ist jeder Hook-Aufruf ein 1:N Python-Funktionsaufruf, wobei N die Anzahl der registrierten Implementierungsfunktionen für eine gegebene Spezifikation ist. Alle Spezifikationen und Implementierungen folgen der pytest_ Präfix-Namenskonvention, was sie leicht unterscheidbar und auffindbar macht.

Reihenfolge der Plugin-Erkennung beim Programmstart

pytest lädt Plugin-Module beim Programmstart wie folgt

  1. durch Scannen der Befehlszeile nach der Option -p no:name und *Blockieren* des Ladens dieses Plugins (selbst eingebaute Plugins können auf diese Weise blockiert werden). Dies geschieht vor der normalen Befehlszeilenanalyse.

  2. durch Laden aller eingebauten Plugins.

  3. durch Scannen der Befehlszeile nach der Option -p name und Laden des angegebenen Plugins. Dies geschieht vor der normalen Befehlszeilenanalyse.

  4. durch Laden aller Plugins, die über die installierten Drittanbieter-Pakete Entry Points registriert wurden, es sei denn, die Umgebungsvariable PYTEST_DISABLE_PLUGIN_AUTOLOAD ist gesetzt.

  5. durch Laden aller Plugins, die über die Umgebungsvariable PYTEST_PLUGINS angegeben wurden.

  6. durch Laden aller "initialen" conftest.py Dateien

    • Bestimmen der Testpfade: angegeben in der Befehlszeile, andernfalls in testpaths, wenn definiert und vom Root-Verzeichnis aus ausgeführt, andernfalls das aktuelle Verzeichnis.

    • Für jeden Testpfad, laden conftest.py und test*/conftest.py relativ zum Verzeichnisteil des Testpfads, falls vorhanden. Bevor eine conftest.py Datei geladen wird, laden Sie conftest.py Dateien in allen ihren Elternverzeichnissen. Nachdem eine conftest.py Datei geladen wurde, laden Sie rekursiv alle Plugins, die in ihrer pytest_plugins Variablen vorhanden sind.

conftest.py: lokale Verzeichnis-spezifische Plugins

Lokale conftest.py Plugins enthalten Verzeichnis-spezifische Hook-Implementierungen. Hook-Sitzungs- und Testausführungsaktivitäten rufen alle Hooks auf, die in conftest.py Dateien näher an der Wurzel des Dateisystems definiert sind. Beispiel für die Implementierung des Hooks pytest_runtest_setup, so dass er für Tests im Unterverzeichnis a, aber nicht für andere Verzeichnisse aufgerufen wird.

a/conftest.py:
    def pytest_runtest_setup(item):
        # called for running each test in 'a' directory
        print("setting up", item)

a/test_sub.py:
    def test_sub():
        pass

test_flat.py:
    def test_flat():
        pass

So könnten Sie es ausführen

pytest test_flat.py --capture=no  # will not show "setting up"
pytest a/test_sub.py --capture=no  # will show "setting up"

Hinweis

Wenn Sie conftest.py Dateien haben, die sich nicht in einem Python-Paketverzeichnis befinden (d.h. eines, das eine __init__.py enthält), dann kann "import conftest" mehrdeutig sein, da es auch andere conftest.py Dateien auf Ihrem PYTHONPATH oder sys.path geben könnte. Es ist daher gute Praxis für Projekte, entweder conftest.py unter einem Paket-Scope zu platzieren oder niemals etwas aus einer conftest.py Datei zu importieren.

Siehe auch: pytest Importmechanismen und sys.path/PYTHONPATH.

Hinweis

Einige Hooks können nicht in conftest.py-Dateien implementiert werden, die nicht initial sind, aufgrund der Art und Weise, wie pytest Plugins während des Starts entdeckt. Sehen Sie die Dokumentation jedes Hooks für Details.

Schreiben Sie Ihr eigenes Plugin

Wenn Sie ein Plugin schreiben möchten, gibt es viele reale Beispiele, von denen Sie kopieren können

Alle diese Plugins implementieren Hooks und/oder Fixtures, um Funktionalität zu erweitern und hinzuzufügen.

Hinweis

Schauen Sie sich unbedingt das exzellente cookiecutter-pytest-plugin Projekt an, das eine cookiecutter-Vorlage zum Erstellen von Plugins ist.

Die Vorlage bietet einen exzellenten Ausgangspunkt mit einem funktionierenden Plugin, Tests, die mit tox laufen, einer umfassenden README-Datei sowie einem vorkonfigurierten Entry Point.

Erwägen Sie auch, Ihr Plugin zu pytest-dev beizutragen, sobald es einige zufriedene Benutzer außer Ihnen selbst hat.

Ihr Plugin für andere installierbar machen

Wenn Sie Ihr Plugin extern verfügbar machen möchten, können Sie einen sogenannten Entry Point für Ihre Distribution definieren, damit pytest Ihr Plugin-Modul findet. Entry Points sind eine Funktion, die von Verpackungswerkzeugen bereitgestellt wird.

pytest sucht nach dem pytest11 Entry Point, um seine Plugins zu entdecken. Daher können Sie Ihr Plugin verfügbar machen, indem Sie es in Ihrer pyproject.toml Datei definieren.

# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "myproject"
classifiers = [
    "Framework :: Pytest",
]

[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"

Wenn ein Paket auf diese Weise installiert wird, lädt pytest myproject.pluginmodule als Plugin, das Hooks definieren kann. Bestätigen Sie die Registrierung mit pytest --trace-config

Hinweis

Stellen Sie sicher, dass Sie Framework :: Pytest in Ihre Liste der PyPI-Klassifikatoren aufnehmen, damit Benutzer Ihr Plugin leicht finden können.

Assertion Rewriting

Eines der Hauptmerkmale von pytest ist die Verwendung von einfachen assert-Anweisungen und die detaillierte Introspektion von Ausdrücken bei Assertionsfehlern. Dies wird durch "Assertion Rewriting" bereitgestellt, das die analysierte AST modifiziert, bevor sie zu Bytecode kompiliert wird. Dies geschieht über einen PEP 302 Import-Hook, der frühzeitig installiert wird, wenn pytest startet, und diese Umwandlung durchführt, wenn Module importiert werden. Da wir jedoch keinen anderen Bytecode testen möchten als den, den Sie in der Produktion ausführen, wandelt dieser Hook nur Testmodule selbst (wie durch die python_files Konfigurationsoption definiert) und alle Module, die Teil von Plugins sind, um. Jedes andere importierte Modul wird nicht umgewandelt, und das normale Assertionsverhalten wird angewendet.

Wenn Sie Assertionshelfer in anderen Modulen haben, bei denen Assertion Rewriting aktiviert sein soll, müssen Sie pytest explizit bitten, dieses Modul vor dem Import umzuwandeln.

register_assert_rewrite(*names)[Quelle]

Registriert einen oder mehrere Modulnamen, die beim Import umgewandelt werden sollen.

Diese Funktion stellt sicher, dass dieses Modul oder alle Module innerhalb des Pakets ihre Assertionsanweisungen umgewandelt bekommen. Sie sollten also sicherstellen, dass Sie dies aufrufen, bevor das Modul tatsächlich importiert wird, normalerweise in Ihrer \_\_init\_\_.py, wenn Sie ein Plugin sind, das ein Paket verwendet.

Parameter:

names (str) – Die zu registrierenden Modulnamen.

Dies ist besonders wichtig, wenn Sie ein pytest-Plugin schreiben, das mit einem Paket erstellt wird. Der Import-Hook behandelt nur conftest.py-Dateien und alle Module, die im pytest11-Entrypoint aufgeführt sind, als Plugins. Als Beispiel betrachten wir das folgende Paket.

pytest_foo/__init__.py
pytest_foo/plugin.py
pytest_foo/helper.py

Mit dem folgenden typischen setup.py-Auszug

setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)

In diesem Fall wird nur pytest_foo/plugin.py umgewandelt. Wenn das Hilfsmodul auch Assertionsanweisungen enthält, die umgewandelt werden müssen, muss es als solches markiert werden, bevor es importiert wird. Dies geschieht am einfachsten, indem es im __init__.py-Modul markiert wird, das immer zuerst importiert wird, wenn ein Modul innerhalb eines Pakets importiert wird. Auf diese Weise kann plugin.py helper.py weiterhin normal importieren. Der Inhalt von pytest_foo/__init__.py muss dann wie folgt aussehen:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

Plugins in einem Testmodul oder einer conftest-Datei anfordern/laden

Sie können Plugins in einem Testmodul oder einer conftest.py-Datei mit pytest_plugins anfordern.

pytest_plugins = ["name1", "name2"]

Wenn das Testmodul oder das conftest-Plugin geladen wird, werden die angegebenen Plugins ebenfalls geladen. Jedes Modul kann als Plugin gekennzeichnet werden, einschließlich interner Anwendungsmodule.

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins werden rekursiv verarbeitet, also beachten Sie, dass im obigen Beispiel, wenn myapp.testsupport.myplugin auch pytest_plugins deklariert, die Inhalte der Variablen ebenfalls als Plugins geladen werden und so weiter.

Hinweis

Das Anfordern von Plugins mithilfe der Variablen pytest_plugins in nicht-root conftest.py-Dateien ist veraltet.

Dies ist wichtig, da conftest.py-Dateien pro Verzeichnis Hook-Implementierungen implementieren, aber sobald ein Plugin importiert ist, wird es den gesamten Verzeichnisbaum beeinflussen. Um Verwirrung zu vermeiden, ist das Definieren von pytest_plugins in jeder conftest.py-Datei, die sich nicht im Stammverzeichnis der Tests befindet, veraltet und löst eine Warnung aus.

Dieser Mechanismus macht es einfach, Fixtures innerhalb von Anwendungen oder sogar externen Anwendungen zu teilen, ohne externe Plugins mithilfe der Entry Point Packaging Metadata Technik erstellen zu müssen.

Plugins, die von pytest_plugins importiert werden, werden ebenfalls automatisch für Assertion Rewriting markiert (siehe pytest.register_assert_rewrite()). Damit dies jedoch Wirkung zeigt, darf das Modul noch nicht importiert worden sein. Wenn es zum Zeitpunkt der Verarbeitung der pytest_plugins Anweisung bereits importiert wurde, wird eine Warnung ausgegeben und Assertions innerhalb des Plugins werden nicht umgewandelt. Um dies zu beheben, können Sie entweder pytest.register_assert_rewrite() selbst aufrufen, bevor das Modul importiert wird, oder Sie können den Code so arrangieren, dass der Import verzögert wird, bis das Plugin registriert ist.

Zugriff auf ein anderes Plugin nach Namen

Wenn ein Plugin mit Code aus einem anderen Plugin zusammenarbeiten möchte, kann es wie folgt über den Plugin-Manager eine Referenz erhalten:

plugin = config.pluginmanager.get_plugin("name_of_plugin")

Wenn Sie die Namen bestehender Plugins anzeigen möchten, verwenden Sie die Option --trace-config.

Registrierung benutzerdefinierter Marker

Wenn Ihr Plugin Marker verwendet, sollten Sie diese registrieren, damit sie in Pytests Hilfetext angezeigt werden und unnötige Warnungen verursachen. Zum Beispiel würde das folgende Plugin cool_marker und mark_with für alle Benutzer registrieren.

def pytest_configure(config):
    config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
    config.addinivalue_line(
        "markers", "mark_with(arg, arg2): this marker takes arguments."
    )

Plugins testen

pytest wird mit einem Plugin namens pytester geliefert, das Ihnen hilft, Tests für Ihren Plugin-Code zu schreiben. Das Plugin ist standardmäßig deaktiviert, daher müssen Sie es aktivieren, bevor Sie es verwenden können.

Sie können dies tun, indem Sie die folgende Zeile zu einer conftest.py-Datei in Ihrem Testverzeichnis hinzufügen.

# content of conftest.py

pytest_plugins = ["pytester"]

Alternativ können Sie pytest mit der Befehlszeilenoption -p pytester aufrufen.

Dadurch können Sie die pytester Fixture zum Testen Ihres Plugin-Codes verwenden.

Lassen Sie uns demonstrieren, was Sie mit dem Plugin machen können, anhand eines Beispiels. Stellen Sie sich vor, wir haben ein Plugin entwickelt, das eine Fixture hello bereitstellt, die eine Funktion liefert und wir können diese Funktion mit einem optionalen Parameter aufrufen. Sie gibt den String-Wert Hello World! zurück, wenn wir keinen Wert angeben, oder Hello {value}!, wenn wir einen String-Wert angeben.

import pytest


def pytest_addoption(parser):
    group = parser.getgroup("helloworld")
    group.addoption(
        "--name",
        action="store",
        dest="name",
        default="World",
        help='Default "name" for hello().',
    )


@pytest.fixture
def hello(request):
    name = request.config.getoption("name")

    def _hello(name=None):
        if not name:
            name = request.config.getoption("name")
        return f"Hello {name}!"

    return _hello

Die pytester Fixture bietet eine praktische API zum Erstellen temporärer conftest.py-Dateien und Testdateien. Sie erlaubt uns auch, die Tests auszuführen und ein Ergebnisobjekt zurückzugeben, mit dem wir die Testergebnisse überprüfen können.

def test_hello(pytester):
    """Make sure that our plugin works."""

    # create a temporary conftest.py file
    pytester.makeconftest(
        """
        import pytest

        @pytest.fixture(params=[
            "Brianna",
            "Andreas",
            "Floris",
        ])
        def name(request):
            return request.param
    """
    )

    # create a temporary pytest test file
    pytester.makepyfile(
        """
        def test_hello_default(hello):
            assert hello() == "Hello World!"

        def test_hello_name(hello, name):
            assert hello(name) == "Hello {0}!".format(name)
    """
    )

    # run all tests with pytest
    result = pytester.runpytest()

    # check that all 4 tests passed
    result.assert_outcomes(passed=4)

Zusätzlich ist es möglich, Beispiele in die isolierte Umgebung von pytester zu kopieren, bevor pytest darauf ausgeführt wird. Auf diese Weise können wir die getestete Logik in separate Dateien abstrahieren, was besonders nützlich für längere Tests und/oder längere conftest.py-Dateien ist.

Beachten Sie, dass für pytester.copy_example die Einstellung von pytester_example_dir in unserer Konfigurationsdatei erforderlich ist, um pytest mitzuteilen, wo nach Beispieldateien gesucht werden soll.

# content of pytest.toml
[pytest]
pytester_example_dir = "."
# content of test_example.py


def test_plugin(pytester):
    pytester.copy_example("test_example.py")
    pytester.runpytest("-k", "test_example")


def test_example():
    pass
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
configfile: pytest.toml
collected 2 items

test_example.py ..                                                   [100%]

============================ 2 passed in 0.12s =============================

Weitere Informationen über das Ergebnisobjekt, das runpytest() zurückgibt, und die Methoden, die es bereitstellt, finden Sie in der Dokumentation zu RunResult.