Wie man Fixtures benutzt¶
Siehe auch
Siehe auch
Fixtures „anfordern“¶
Auf einer grundlegenden Ebene fordern Testfunktionen die Fixtures an, die sie benötigen, indem sie diese als Argumente deklarieren.
Wenn pytest einen Test ausführt, schaut es sich die Parameter in der Signatur dieser Testfunktion an und sucht dann nach Fixtures, die die gleichen Namen wie diese Parameter haben. Sobald pytest sie gefunden hat, führt es diese Fixtures aus, fängt auf, was sie zurückgegeben haben (falls etwas), und übergibt diese Objekte als Argumente an die Testfunktion.
Schnelles Beispiel¶
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
In diesem Beispiel „fordert“ test_fruit_salad fruit_bowl an (d.h. def test_fruit_salad(fruit_bowl):), und wenn pytest dies sieht, wird es die fruit_bowl Fixture-Funktion ausführen und das von ihr zurückgegebene Objekt an test_fruit_salad als fruit_bowl Argument übergeben.
Hier ist ungefähr, was passiert, wenn wir es manuell machen würden
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)
Fixtures können andere Fixtures „anfordern“¶
Eine der größten Stärken von pytest ist sein extrem flexibles Fixture-System. Es ermöglicht uns, komplexe Anforderungen für Tests in einfachere und organisiertere Funktionen zu zerlegen, bei denen jede Funktion nur beschreiben muss, wovon sie abhängig ist. Wir werden uns damit weiter unten beschäftigen, aber hier ist zunächst ein schnelles Beispiel, das zeigt, wie Fixtures andere Fixtures verwenden können.
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
Beachten Sie, dass dies dasselbe Beispiel wie oben ist, aber nur wenig geändert wurde. Fixtures in pytest „fordern“ Fixtures genauso an wie Tests. Alle gleichen „Anforderungs“-Regeln gelten für Fixtures, die für Tests gelten. So würde dieses Beispiel funktionieren, wenn wir es manuell machen würden:
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
Fixtures sind wiederverwendbar¶
Eines der Dinge, die das Fixture-System von pytest so leistungsfähig machen, ist, dass es uns die Möglichkeit gibt, einen generischen Einrichtungsschritt zu definieren, der immer wieder verwendet werden kann, genau wie eine normale Funktion verwendet würde. Zwei verschiedene Tests können dasselbe Fixture anfordern und pytest wird jedem Test sein eigenes Ergebnis von diesem Fixture geben.
Das ist extrem nützlich, um sicherzustellen, dass Tests sich nicht gegenseitig beeinflussen. Wir können dieses System nutzen, um sicherzustellen, dass jeder Test seine eigene frische Charge von Daten erhält und mit einem sauberen Zustand beginnt, damit er konsistente, wiederholbare Ergebnisse liefern kann.
Hier ist ein Beispiel, wie das nützlich sein kann
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
Jeder Test hier erhält seine eigene Kopie des list Objekts, was bedeutet, dass das order Fixture zweimal ausgeführt wird (dasselbe gilt für das first_entry Fixture). Wenn wir dies auch manuell machen würden, sähe es ungefähr so aus:
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
entry = first_entry()
the_list = order(first_entry=entry)
test_int(order=the_list)
Ein Test/Fixture kann mehr als ein Fixture gleichzeitig „anfordern“¶
Tests und Fixtures sind nicht darauf beschränkt, jeweils nur ein Fixture „anzufordern“. Sie können so viele anfordern, wie sie möchten. Hier ist ein weiteres schnelles Beispiel zur Veranschaulichung.
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def second_entry():
return 2
# Arrange
@pytest.fixture
def order(first_entry, second_entry):
return [first_entry, second_entry]
# Arrange
@pytest.fixture
def expected_list():
return ["a", 2, 3.0]
def test_string(order, expected_list):
# Act
order.append(3.0)
# Assert
assert order == expected_list
Fixtures können pro Test mehr als einmal „angefordert“ werden (Rückgabewerte werden zwischengespeichert)¶
Fixtures können auch während desselben Tests mehr als einmal „angefordert“ werden, und pytest führt sie für diesen Test nicht erneut aus. Das bedeutet, wir können Fixtures in mehreren Fixtures „anfordern“, die von ihnen abhängig sind (und sogar wieder im Test selbst), ohne dass diese Fixtures mehr als einmal ausgeführt werden.
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order():
return []
# Act
@pytest.fixture
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(append_first, order, first_entry):
# Assert
assert order == [first_entry]
Wenn ein „angefordertes“ Fixture einmal für jede Zeit ausgeführt würde, in der es während eines Tests „angefordert“ wurde, dann würde dieser Test fehlschlagen, weil sowohl append_first als auch test_string_only order als leere Liste (d.h. []) sehen würden, aber da der Rückgabewert von order nach dem ersten Aufruf zwischengespeichert wurde (zusammen mit allen Nebeneffekten, die seine Ausführung haben könnte), bezogen sich sowohl der Test als auch append_first auf dasselbe Objekt, und der Test sah den Effekt, den append_first auf dieses Objekt hatte.
Autouse-Fixtures (Fixtures, die man nicht anfordern muss)¶
Manchmal möchten Sie vielleicht ein Fixture (oder sogar mehrere) haben, von dem Sie wissen, dass alle Ihre Tests davon abhängen werden. „Autouse“-Fixtures sind eine bequeme Möglichkeit, alle Tests automatisch dazu zu bringen, sie „anzufordern“. Dies kann viele redundante „Anforderungen“ einsparen und sogar fortgeschrittenere Fixture-Nutzung bieten (mehr dazu später).
Wir können ein Fixture zu einem Autouse-Fixture machen, indem wir autouse=True an den Dekorator des Fixtures übergeben. Hier ist ein einfaches Beispiel, wie sie verwendet werden können.
# contents of test_append.py
import pytest
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return []
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(order, first_entry):
assert order == [first_entry]
def test_string_and_int(order, first_entry):
order.append(2)
assert order == [first_entry, 2]
In diesem Beispiel ist das append_first Fixture ein Autouse-Fixture. Da es automatisch geschieht, sind beide Tests davon betroffen, obwohl keiner der Tests es „angefordert“ hat. Das bedeutet jedoch nicht, dass sie es nicht tun *können*; es ist nur nicht *notwendig*.
Scope: Fixtures über Klassen, Module, Pakete oder die Session hinweg teilen¶
Fixtures, die Netzwerkzugriff benötigen, hängen von der Konnektivität ab und sind normalerweise zeitaufwendig zu erstellen. Um das vorherige Beispiel zu erweitern, können wir einen scope="module" Parameter an die @pytest.fixture-Aufruf übergeben, um zu bewirken, dass eine smtp_connection Fixture-Funktion, die für die Erstellung einer Verbindung zu einem bereits existierenden SMTP-Server zuständig ist, nur einmal pro Test-Modul aufgerufen wird (der Standard ist, einmal pro Test-Funktion aufzurufen). Mehrere Testfunktionen in einem Testmodul erhalten somit jeweils dieselbe smtp_connection Fixture-Instanz, was Zeit spart. Mögliche Werte für scope sind: function, class, module, package oder session.
Das nächste Beispiel platziert die Fixture-Funktion in eine separate conftest.py Datei, damit Tests aus mehreren Testmodulen im Verzeichnis auf die Fixture-Funktion zugreifen können.
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
Hier benötigt test_ehlo den smtp_connection Fixture-Wert. pytest wird die mit @pytest.fixture markierte smtp_connection Fixture-Funktion entdecken und aufrufen. Das Ausführen des Tests sieht so aus:
$ pytest test_module.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_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:7: AssertionError
________________________________ test_noop _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================
Sie sehen die beiden fehlgeschlagenen assert 0 und, was noch wichtiger ist, Sie sehen auch, dass dasselbe smtp_connection Objekt an die beiden Testfunktionen übergeben wurde, da pytest die eingehenden Argumentwerte im Traceback anzeigt. Infolgedessen laufen die beiden Testfunktionen, die smtp_connection verwenden, so schnell wie eine einzelne, da sie dieselbe Instanz wiederverwenden.
Wenn Sie sich entscheiden, dass Sie lieber eine Session-Scope-Instanz von smtp_connection haben möchten, können Sie sie einfach deklarieren.
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests requesting it
...
Fixture-Scopes¶
Fixtures werden erstellt, wenn sie zum ersten Mal von einem Test angefordert werden, und sie werden basierend auf ihrem scope zerstört.
function: Der Standard-Scope. Das Fixture wird am Ende des Tests zerstört.class: Das Fixture wird während des Teardowns des letzten Tests in der Klasse zerstört.module: Das Fixture wird während des Teardowns des letzten Tests im Modul zerstört.package: Das Fixture wird während des Teardowns des letzten Tests im Paket zerstört, in dem das Fixture definiert ist, einschließlich Unterpaketen und Unterverzeichnissen darin.session: Das Fixture wird am Ende der Testsitzung zerstört.
Hinweis
Pytest speichert nur eine Instanz eines Fixtures gleichzeitig im Cache, was bedeutet, dass bei der Verwendung eines parametrisierten Fixtures pytest ein Fixture möglicherweise mehr als einmal im gegebenen Scope aufruft.
Dynamischer Scope¶
Hinzugefügt in Version 5.2.
In einigen Fällen möchten Sie vielleicht den Scope eines Fixtures ändern, ohne den Code zu ändern. Um dies zu tun, übergeben Sie einen aufrufbaren Wert an scope. Der aufrufbare Wert muss einen String mit einem gültigen Scope zurückgeben und wird nur einmal ausgeführt – während der Fixture-Definition. Er wird mit zwei Schlüsselwortargumenten aufgerufen: fixture_name als String und config mit einem Konfigurationsobjekt.
Dies kann besonders nützlich sein, wenn es um Fixtures geht, die Zeit für die Einrichtung benötigen, wie z.B. das Starten eines Docker-Containers. Sie können das Kommandozeilenargument verwenden, um den Scope der gestarteten Container für verschiedene Umgebungen zu steuern. Sehen Sie sich das folgende Beispiel an.
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
Teardown/Bereinigung (AKA Fixture-Finalisierung)¶
Wenn wir unsere Tests ausführen, möchten wir sicherstellen, dass sie sich selbst aufräumen, damit sie keine anderen Tests beeinträchtigen (und auch damit wir keinen Berg von Testdaten zurücklassen, der das System aufbläht). Fixtures in pytest bieten ein sehr nützliches Teardown-System, das es uns ermöglicht, die spezifischen Schritte zu definieren, die für jedes Fixture zur Bereinigung notwendig sind.
Dieses System kann auf zwei Arten genutzt werden.
1. yield-Fixtures (empfohlen)¶
„Yield“-Fixtures geben mit yield anstelle von return zurück. Mit diesen Fixtures können wir Code ausführen und ein Objekt an das anfordernde Fixture/den Test zurückgeben, genau wie bei den anderen Fixtures. Die einzigen Unterschiede sind:
returnwird durchyieldersetzt.Jeglicher Teardown-Code für dieses Fixture wird *nach* dem
yieldplatziert.
Sobald pytest eine lineare Reihenfolge für die Fixtures ermittelt hat, führt es jedes nacheinander aus, bis es zurückkehrt oder yieldet, und geht dann zum nächsten Fixture in der Liste über, um dasselbe zu tun.
Sobald der Test abgeschlossen ist, geht pytest die Liste der Fixtures zurück, aber in *umgekehrter Reihenfolge*, nimmt jedes, das yieldet hat, und führt den Code darin aus, der sich *nach* der yield-Anweisung befand.
Als einfaches Beispiel betrachten wir dieses grundlegende E-Mail-Modul.
# content of emaillib.py
class MailAdminClient:
def create_user(self):
return MailUser()
def delete_user(self, user):
# do some cleanup
pass
class MailUser:
def __init__(self):
self.inbox = []
def send_email(self, email, other):
other.inbox.append(email)
def clear_mailbox(self):
self.inbox.clear()
class Email:
def __init__(self, subject, body):
self.subject = subject
self.body = body
Nehmen wir an, wir wollen das Senden einer E-Mail von einem Benutzer an einen anderen testen. Wir müssen zuerst jeden Benutzer erstellen, dann die E-Mail von einem Benutzer an den anderen senden und schließlich behaupten, dass der andere Benutzer diese Nachricht in seiner Inbox erhalten hat. Wenn wir nach dem Test aufräumen wollen, müssen wir wahrscheinlich sicherstellen, dass das Postfach des anderen Benutzers geleert wird, bevor dieser Benutzer gelöscht wird, sonst kann das System sich beschweren.
Hier ist, wie das aussehen könnte.
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox()
mail_admin.delete_user(user)
def test_email_received(sending_user, receiving_user):
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
assert email in receiving_user.inbox
Da receiving_user das letzte Fixture ist, das während des Setups ausgeführt wird, ist es das erste, das während des Teardowns ausgeführt wird.
Es besteht die Gefahr, dass selbst die richtige Reihenfolge auf der Teardown-Seite keine sichere Bereinigung garantiert. Dies wird in Sichere Teardowns etwas detaillierter behandelt.
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
Fehlerbehandlung für Yield-Fixtures¶
Wenn ein Yield-Fixture vor dem Yielden eine Ausnahme auslöst, wird pytest nicht versuchen, den Teardown-Code nach der yield-Anweisung dieses Fixtures auszuführen. Aber für jedes Fixture, das für diesen Test bereits erfolgreich ausgeführt wurde, wird pytest dennoch versuchen, es wie gewohnt abzubauen.
2. Finalizer direkt hinzufügen¶
Während Yield-Fixtures als die sauberere und geradlinigere Option gelten, gibt es eine andere Wahl, und das ist das Hinzufügen von „Finalizer“-Funktionen direkt zum request-context des Tests. Es führt zu einem ähnlichen Ergebnis wie Yield-Fixtures, erfordert aber etwas mehr Ausführlichkeit.
Um diesen Ansatz zu verwenden, müssen wir das request-context Objekt (genauso wie wir ein anderes Fixture anfordern würden) in dem Fixture anfordern, für das wir Teardown-Code hinzufügen müssen, und dann einen aufrufbaren Wert, der diesen Teardown-Code enthält, an seine addfinalizer Methode übergeben.
Wir müssen jedoch vorsichtig sein, da pytest diesen Finalizer ausführt, sobald er hinzugefügt wurde, auch wenn das Fixture nach dem Hinzufügen des Finalizers eine Ausnahme auslöst. Um also sicherzustellen, dass wir den Finalizer-Code nicht ausführen, wenn er nicht benötigt wird, würden wir den Finalizer nur hinzufügen, wenn das Fixture etwas getan hätte, das wir abbauen müssten.
Hier ist, wie das vorherige Beispiel unter Verwendung der addfinalizer Methode aussehen würde.
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin, request):
user = mail_admin.create_user()
def delete_user():
mail_admin.delete_user(user)
request.addfinalizer(delete_user)
return user
@pytest.fixture
def email(sending_user, receiving_user, request):
_email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(_email, receiving_user)
def empty_mailbox():
receiving_user.clear_mailbox()
request.addfinalizer(empty_mailbox)
return _email
def test_email_received(receiving_user, email):
assert email in receiving_user.inbox
Es ist etwas länger als Yield-Fixtures und etwas komplexer, aber es bietet einige Nuancen, wenn Sie in der Klemme stecken.
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
Hinweis zur Reihenfolge der Finalizer¶
Finalizer werden in einer Last-In-First-Out-Reihenfolge ausgeführt. Bei Yield-Fixtures ist der erste Teardown-Code, der ausgeführt wird, von dem rechtesten Fixture, d.h. dem letzten Testparameter.
# content of test_finalizers.py
import pytest
def test_bar(fix_w_yield1, fix_w_yield2):
print("test_bar")
@pytest.fixture
def fix_w_yield1():
yield
print("after_yield_1")
@pytest.fixture
def fix_w_yield2():
yield
print("after_yield_2")
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.after_yield_2
after_yield_1
============================ 1 passed in 0.12s =============================
Bei Finalizern ist das erste Fixture, das ausgeführt wird, der letzte Aufruf von request.addfinalizer.
# content of test_finalizers.py
from functools import partial
import pytest
@pytest.fixture
def fix_w_finalizers(request):
request.addfinalizer(partial(print, "finalizer_2"))
request.addfinalizer(partial(print, "finalizer_1"))
def test_bar(fix_w_finalizers):
print("test_bar")
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.finalizer_1
finalizer_2
============================ 1 passed in 0.12s =============================
Dies liegt daran, dass Yield-Fixtures im Hintergrund addfinalizer verwenden: Wenn das Fixture ausgeführt wird, registriert addfinalizer eine Funktion, die den Generator fortsetzt, was wiederum den Teardown-Code aufruft.
Sichere Teardowns¶
Das Fixture-System von pytest ist *sehr* mächtig, aber es wird immer noch von einem Computer ausgeführt, daher kann es nicht herausfinden, wie es alles, was wir ihm zumuten, sicher abbauen kann. Wenn wir nicht vorsichtig sind, könnte ein Fehler an der falschen Stelle Reste von unseren Tests hinterlassen, und das kann schnell zu weiteren Problemen führen.
Betrachten Sie zum Beispiel die folgenden Tests (basierend auf dem E-Mail-Beispiel von oben).
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def setup():
mail_admin = MailAdminClient()
sending_user = mail_admin.create_user()
receiving_user = mail_admin.create_user()
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
yield receiving_user, email
receiving_user.clear_mailbox()
mail_admin.delete_user(sending_user)
mail_admin.delete_user(receiving_user)
def test_email_received(setup):
receiving_user, email = setup
assert email in receiving_user.inbox
Diese Version ist viel kompakter, aber auch schwerer zu lesen, hat keinen sehr beschreibenden Fixture-Namen, und keines der Fixtures kann leicht wiederverwendet werden.
Es gibt auch ein ernsteres Problem: Wenn einer dieser Schritte im Setup eine Ausnahme auslöst, wird keiner der Teardown-Codes ausgeführt.
Eine Option könnte darin bestehen, die Methode addfinalizer anstelle von Yield-Fixtures zu verwenden, aber das könnte ziemlich komplex und schwer zu warten werden (und es wäre nicht mehr kompakt).
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
Sichere Fixture-Struktur¶
Die sicherste und einfachste Fixture-Struktur erfordert, dass Fixtures nur eine zustandsändernde Aktion pro Stück ausführen und diese dann mit ihrem Teardown-Code bündeln, wie die obigen E-Mail-Beispiele gezeigt haben.
Die Wahrscheinlichkeit, dass eine zustandsändernde Operation fehlschlägt, aber dennoch den Zustand ändert, ist vernachlässigbar, da die meisten dieser Operationen transaktionsbasiert sind (zumindest auf der Testebene, auf der Zustände zurückbleiben könnten). Wenn wir also sicherstellen, dass jede erfolgreiche zustandsändernde Aktion abgebaut wird, indem wir sie in eine separate Fixture-Funktion verschieben und sie von anderen, potenziell fehlschlagenden zustandsändernden Aktionen trennen, dann haben unsere Tests die beste Chance, die Testumgebung so zu hinterlassen, wie sie sie vorgefunden haben.
Nehmen wir als Beispiel an, wir haben eine Website mit einer Login-Seite und wir haben Zugriff auf eine Admin-API, über die wir Benutzer erstellen können. Für unseren Test wollen wir
Einen Benutzer über diese Admin-API erstellen
Einen Browser mit Selenium starten
Zur Login-Seite unserer Website gehen
Sich als der erstellte Benutzer anmelden
Bestätigen, dass sein Name in der Kopfzeile der Landingpage steht
Wir möchten diesen Benutzer nicht im System belassen und auch die Browsersitzung nicht offen lassen, daher wollen wir sicherstellen, dass die Fixtures, die diese Dinge erstellen, sich selbst aufräumen.
Hier ist, wie das aussehen könnte.
Hinweis
Für dieses Beispiel wird davon ausgegangen, dass bestimmte Fixtures (d.h. base_url und admin_credentials) woanders existieren. Vorerst gehen wir also davon aus, dass sie existieren und wir sie nur nicht betrachten.
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture
def login(driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
@pytest.fixture
def landing_page(driver, login):
return LandingPage(driver)
def test_name_on_landing_page_after_login(landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
Die Art und Weise, wie die Abhängigkeiten angelegt sind, lässt unklar, ob das user Fixture vor dem driver Fixture ausgeführt wird. Aber das ist in Ordnung, weil das atomare Operationen sind, und es spielt keine Rolle, welche zuerst läuft, da die Ereignisreihenfolge für den Test immer noch linearisierbar ist. Aber was wichtig ist, ist, dass, egal welche zuerst läuft, wenn die eine eine Ausnahme auslöst, während die andere das nicht getan hätte, keine etwas zurückgelassen hat. Wenn driver vor user ausgeführt wird und user eine Ausnahme auslöst, wird der Treiber immer noch beendet, und der Benutzer wurde nie erstellt. Und wenn driver derjenige war, der die Ausnahme auslöst, dann wäre der Treiber nie gestartet worden und der Benutzer wäre nie erstellt worden.
Mehrere assert-Anweisungen sicher ausführen¶
Manchmal möchten Sie vielleicht mehrere Assertions ausführen, nachdem Sie all diese Einrichtung vorgenommen haben, was sinnvoll ist, da in komplexeren Systemen eine einzelne Aktion mehrere Verhaltensweisen auslösen kann. pytest bietet eine bequeme Möglichkeit, dies zu handhaben, und es kombiniert vieles von dem, was wir bisher besprochen haben.
Alles, was benötigt wird, ist, zu einem größeren Scope aufzusteigen, dann den **act**-Schritt als Autouse-Fixture zu definieren und schließlich sicherzustellen, dass alle Fixtures auf diesen höheren Scope abzielen.
Nehmen wir ein Beispiel von oben und passen es ein wenig an. Nehmen wir an, wir wollen zusätzlich zur Überprüfung einer Willkommensnachricht in der Kopfzeile auch nach einem Abmelde-Button und einem Link zum Profil des Benutzers suchen.
Schauen wir uns an, wie wir das strukturieren können, damit wir mehrere Assertions ausführen können, ohne all diese Schritte wiederholen zu müssen.
Hinweis
Für dieses Beispiel wird davon ausgegangen, dass bestimmte Fixtures (d.h. base_url und admin_credentials) woanders existieren. Vorerst gehen wir also davon aus, dass sie existieren und wir sie nur nicht betrachten.
# contents of tests/end_to_end/test_login.py
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture(scope="class")
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture(scope="class")
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture(scope="class")
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture(scope="class")
def landing_page(driver, login):
return LandingPage(driver)
class TestLandingPageSuccess:
@pytest.fixture(scope="class", autouse=True)
def login(self, driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
def test_name_in_header(self, landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
def test_sign_out_button(self, landing_page):
assert landing_page.sign_out_button.is_displayed()
def test_profile_link(self, landing_page, user):
profile_href = urljoin(base_url, f"/profile?id={user.profile_id}")
assert landing_page.profile_link.get_attribute("href") == profile_href
Beachten Sie, dass die Methoden nur self in der Signatur als Formalität referenzieren. Kein Zustand ist an die eigentliche Testklasse gebunden, wie es im unittest.TestCase Framework der Fall sein könnte. Alles wird vom pytest Fixture-System verwaltet.
Jede Methode muss nur die Fixtures anfordern, die sie tatsächlich benötigt, ohne sich um die Reihenfolge kümmern zu müssen. Das liegt daran, dass das **act** Fixture ein Autouse-Fixture ist und es sichergestellt hat, dass alle anderen Fixtures vor ihm ausgeführt wurden. Es müssen keine weiteren Zustandsänderungen mehr stattfinden, daher können die Tests so viele nicht-zustandsändernde Abfragen machen, wie sie wollen, ohne zu riskieren, die anderen Tests zu beeinträchtigen.
Das login Fixture ist ebenfalls innerhalb der Klasse definiert, da nicht jeder der anderen Tests im Modul eine erfolgreiche Anmeldung erwarten wird, und das **act** muss für eine andere Testklasse möglicherweise etwas anders behandelt werden. Wenn wir zum Beispiel ein weiteres Testszenario rund um die Übermittlung falscher Anmeldedaten schreiben möchten, könnten wir dies tun, indem wir etwas wie folgt zu der Testdatei hinzufügen:
class TestLandingPageBadCredentials:
@pytest.fixture(scope="class")
def faux_user(self, user):
_user = deepcopy(user)
_user.password = "badpass"
return _user
def test_raises_bad_credentials_exception(self, login_page, faux_user):
with pytest.raises(BadCredentialsException):
login_page.login(faux_user)
Fixtures können den anfordernden Testkontext introspektieren¶
Fixture-Funktionen können das request Objekt akzeptieren, um den „anfordernden“ Testfunktions-, Klassen- oder Modulkontext zu introspektieren. Um das vorherige smtp_connection Fixture-Beispiel weiter zu erweitern, lesen wir eine optionale Server-URL aus dem Testmodul, das unser Fixture verwendet.
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection} ({server})")
smtp_connection.close()
Wir verwenden das request.module Attribut, um optional ein smtpserver Attribut aus dem Testmodul zu erhalten. Wenn wir einfach erneut ausführen, hat sich nicht viel geändert.
$ pytest -s -q --tb=no test_module.py
FFfinalizing <smtplib.SMTP object at 0xdeadbeef0002> (smtp.gmail.com)
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
2 failed in 0.12s
Erstellen wir schnell ein weiteres Testmodul, das tatsächlich die Server-URL in seinem Modul-Namespace festlegt.
# content of test_anothersmtp.py
smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp_connection):
assert 0, smtp_connection.helo()
Ausführen
$ pytest -qq --tb=short test_anothersmtp.py
F [100%]
================================= FAILURES =================================
______________________________ test_showhelo _______________________________
test_anothersmtp.py:6: in test_showhelo
assert 0, smtp_connection.helo()
E AssertionError: (250, b'mail.python.org')
E assert 0
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0003> (mail.python.org)
========================= short test summary info ==========================
FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....
Voilà! Die smtp_connection Fixture-Funktion hat unseren Mailserver-Namen aus dem Modul-Namespace übernommen.
Verwenden von Markern zum Übergeben von Daten an Fixtures¶
Mithilfe des request-Objekts kann ein Fixture auch auf Marker zugreifen, die auf eine Testfunktion angewendet werden. Dies kann nützlich sein, um Daten von einem Test an ein Fixture zu übergeben.
import pytest
@pytest.fixture
def fixt(request):
marker = request.node.get_closest_marker("fixt_data")
if marker is None:
# Handle missing marker in some way...
data = None
else:
data = marker.args[0]
# Do something with the data
return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
assert fixt == 42
Fabriken als Fixtures¶
Das „Factory as Fixture“-Muster kann in Situationen hilfreich sein, in denen das Ergebnis eines Fixtures mehrmals in einem einzelnen Test benötigt wird. Anstatt die Daten direkt zurückzugeben, gibt das Fixture stattdessen eine Funktion zurück, die die Daten generiert. Diese Funktion kann dann mehrmals im Test aufgerufen werden.
Fabriken können bei Bedarf Parameter haben.
@pytest.fixture
def make_customer_record():
def _make_customer_record(name):
return {"name": name, "orders": []}
return _make_customer_record
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
Wenn die von der Fabrik erzeugten Daten verwaltet werden müssen, kann das Fixture dies übernehmen.
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
Parametrisieren von Fixtures¶
Fixture-Funktionen können parametrisiert werden, in diesem Fall werden sie mehrmals aufgerufen, wobei jedes Mal die Menge der abhängigen Tests ausgeführt wird, d.h. die Tests, die von diesem Fixture abhängen. Testfunktionen müssen normalerweise nicht wissen, dass sie erneut ausgeführt werden. Fixture-Parametrisierung hilft beim Schreiben von umfassenden Funktionstests für Komponenten, die selbst auf vielfältige Weise konfiguriert werden können.
Um das vorherige Beispiel zu erweitern, können wir das Fixture so kennzeichnen, dass es zwei smtp_connection Fixture-Instanzen erstellt, was dazu führt, dass alle Tests, die das Fixture verwenden, zweimal ausgeführt werden. Die Fixture-Funktion erhält über das spezielle request-Objekt Zugriff auf jeden Parameter.
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection}")
smtp_connection.close()
Die Hauptänderung ist die Deklaration von params mit @pytest.fixture, einer Liste von Werten, für jeden von denen die Fixture-Funktion ausgeführt wird und auf einen Wert über request.param zugreifen kann. Kein Testfunktionscode muss geändert werden. Also, machen wir noch eine Ausführung.
$ pytest -q test_module.py
FFFF [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:7: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
________________________ test_ehlo[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert b"smtp.gmail.com" in msg
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'
test_module.py:6: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0004>
________________________ test_noop[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0005>
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
FAILED test_module.py::test_noop[mail.python.org] - assert 0
4 failed in 0.12s
Wir sehen, dass unsere beiden Testfunktionen jeweils zweimal ausgeführt wurden, gegen die verschiedenen smtp_connection Instanzen. Beachten Sie auch, dass mit der Verbindung mail.python.org der zweite Test in test_ehlo fehlschlägt, da ein anderer Server-String erwartet wird als der, der ankam.
pytest erstellt einen String, der die Test-ID für jeden Fixture-Wert in einem parametrisierten Fixture ist, z.B. test_ehlo[smtp.gmail.com] und test_ehlo[mail.python.org] in den obigen Beispielen. Diese IDs können mit -k verwendet werden, um bestimmte Fälle auszuwählen, und sie identifizieren auch den spezifischen Fall, wenn einer fehlschlägt. Das Ausführen von pytest mit --collect-only zeigt die generierten IDs.
Zahlen, Zeichenketten, Booleans und None verwenden ihre übliche Zeichenkettendarstellung in der Test-ID. Für andere Objekte erstellt pytest eine Zeichenkette basierend auf dem Argumentnamen. Es ist möglich, die für eine Test-ID verwendete Zeichenkette für einen bestimmten Fixture-Wert anzupassen, indem das Schlüsselwortargument ids verwendet wird.
# content of test_ids.py
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
Das Obige zeigt, wie ids entweder eine Liste von Zeichenketten zum Verwenden sein kann oder eine Funktion, die mit dem Fixture-Wert aufgerufen wird und dann eine zu verwendende Zeichenkette zurückgeben muss. Im letzteren Fall, wenn die Funktion None zurückgibt, wird die automatisch generierte ID von pytest verwendet.
Das Ausführen der obigen Tests führt zu den folgenden Test-IDs:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 12 items
<Dir fixtures.rst-232>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
<Module test_emaillib.py>
<Function test_email_received>
<Module test_finalizers.py>
<Function test_bar>
<Module test_ids.py>
<Function test_a[spam]>
<Function test_a[ham]>
<Function test_b[eggs]>
<Function test_b[1]>
<Module test_module.py>
<Function test_ehlo[smtp.gmail.com]>
<Function test_noop[smtp.gmail.com]>
<Function test_ehlo[mail.python.org]>
<Function test_noop[mail.python.org]>
======================= 12 tests collected in 0.12s ========================
Verwenden von Marks mit parametrisierten Fixtures¶
pytest.param() kann verwendet werden, um Marks in Wertebereichen von parametrisierten Fixtures anzuwenden, auf die gleiche Weise, wie sie mit @pytest.mark.parametrize verwendet werden können.
Beispiel
# content of test_fixture_marks.py
import pytest
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
return request.param
def test_data(data_set):
pass
Das Ausführen dieses Tests wird die Ausführung von data_set mit dem Wert 2 *überspringen*.
$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 3 items
test_fixture_marks.py::test_data[0] PASSED [ 33%]
test_fixture_marks.py::test_data[1] PASSED [ 66%]
test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%]
======================= 2 passed, 1 skipped in 0.12s =======================
Modularität: Verwendung von Fixtures aus einer Fixture-Funktion¶
Neben der Verwendung von Fixtures in Testfunktionen können Fixture-Funktionen selbst andere Fixtures verwenden. Dies trägt zu einem modularen Design Ihrer Fixtures bei und ermöglicht die Wiederverwendung Framework-spezifischer Fixtures in vielen Projekten. Als einfaches Beispiel können wir das vorherige Beispiel erweitern und ein Objekt app instanziieren, in das wir die bereits definierte Ressource smtp_connection einfügen.
# content of test_appsetup.py
import pytest
class App:
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
Hier deklarieren wir eine app-Fixture, die die zuvor definierte smtp_connection-Fixture empfängt und damit ein App-Objekt instanziiert. Lassen Sie uns sie ausführen.
$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 2 items
test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
============================ 2 passed in 0.12s =============================
Aufgrund der Parametrisierung von smtp_connection wird der Test zweimal mit zwei verschiedenen App-Instanzen und den entsprechenden SMTP-Servern ausgeführt. Die app-Fixture muss die Parametrisierung von smtp_connection nicht kennen, da pytest den Abhängigkeitsgraphen der Fixtures vollständig analysiert.
Beachten Sie, dass die app-Fixture einen Geltungsbereich (Scope) von module hat und eine Modul-bezogene smtp_connection-Fixture verwendet. Das Beispiel würde immer noch funktionieren, wenn smtp_connection auf einer session-Scope gecacht wäre: Es ist in Ordnung, wenn Fixtures "breitere" Scopes verwenden, aber nicht umgekehrt: Eine Session-bezogene Fixture könnte eine Modul-bezogene nicht sinnvoll verwenden.
Automatische Gruppierung von Tests nach Fixture-Instanzen¶
pytest minimiert die Anzahl der aktiven Fixtures während Testläufen. Wenn Sie eine parametrisierte Fixture haben, werden alle Tests, die sie verwenden, zuerst mit einer Instanz ausgeführt, und dann werden die Finalizer aufgerufen, bevor die nächste Fixture-Instanz erstellt wird. Dies erleichtert unter anderem das Testen von Anwendungen, die globale Zustände erstellen und verwenden.
Das folgende Beispiel verwendet zwei parametrisierte Fixtures, von denen eine Modul-bezogen ist, und alle Funktionen führen print-Aufrufe aus, um den Setup/Teardown-Fluss zu zeigen.
# content of test_module.py
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print(" SETUP modarg", param)
yield param
print(" TEARDOWN modarg", param)
@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
param = request.param
print(" SETUP otherarg", param)
yield param
print(" TEARDOWN otherarg", param)
def test_0(otherarg):
print(" RUN test0 with otherarg", otherarg)
def test_1(modarg):
print(" RUN test1 with modarg", modarg)
def test_2(otherarg, modarg):
print(f" RUN test2 with otherarg {otherarg} and modarg {modarg}")
Lassen Sie uns die Tests im ausführlichen Modus ausführen und die Print-Ausgabe betrachten.
$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1
PASSED
test_module.py::test_2[mod1-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod1-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2
RUN test1 with modarg mod2
PASSED
test_module.py::test_2[mod2-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod2-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2
PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2
============================ 8 passed in 0.12s =============================
Sie sehen, dass die parametrisierte Modul-bezogene Ressource modarg zu einer Reihenfolge der Testausführung geführt hat, die die geringstmögliche Anzahl "aktiver" Ressourcen ergab. Der Finalizer für die parametrisierte Ressource mod1 wurde ausgeführt, bevor die Ressource mod2 eingerichtet wurde.
Beachten Sie insbesondere, dass test_0 völlig unabhängig ist und zuerst abgeschlossen wird. Dann wird test_1 mit mod1 ausgeführt, dann test_2 mit mod1, dann test_1 mit mod2 und schließlich test_2 mit mod2.
Die parametrisierte Ressource otherarg (mit Funktions-Scope) wurde vor jedem Test, der sie verwendete, eingerichtet und danach abgebaut.
Verwenden von Fixtures in Klassen und Modulen mit usefixtures¶
Manchmal benötigen Testfunktionen keinen direkten Zugriff auf ein Fixture-Objekt. Beispielsweise müssen Tests möglicherweise mit einem leeren Verzeichnis als aktuelles Arbeitsverzeichnis arbeiten, interessieren sich aber sonst nicht für das konkrete Verzeichnis. Hier ist, wie Sie die Standard- tempfile und pytest-Fixtures verwenden können, um dies zu erreichen. Wir trennen die Erstellung des Fixtures in eine conftest.py-Datei.
# content of conftest.py
import os
import tempfile
import pytest
@pytest.fixture
def cleandir():
with tempfile.TemporaryDirectory() as newpath:
old_cwd = os.getcwd()
os.chdir(newpath)
yield
os.chdir(old_cwd)
und deklarieren ihre Verwendung in einem Testmodul über einen usefixtures-Marker.
# content of test_setenv.py
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w", encoding="utf-8") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):
assert os.listdir(os.getcwd()) == []
Aufgrund des usefixtures-Markers wird die cleandir-Fixture für die Ausführung jeder Testmethode benötigt, so als ob Sie jedem von ihnen ein Argument "cleandir" übergeben würden. Lassen Sie es uns ausführen, um zu überprüfen, ob unsere Fixture aktiviert ist und die Tests erfolgreich sind.
$ pytest -q
.. [100%]
2 passed in 0.12s
Sie können mehrere Fixtures auf diese Weise angeben.
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test(): ...
und Sie können die Verwendung von Fixtures auf Modul-Ebene mithilfe von pytestmark angeben.
pytestmark = pytest.mark.usefixtures("cleandir")
Es ist auch möglich, Fixtures, die für alle Tests in Ihrem Projekt benötigt werden, in eine Konfigurationsdatei zu legen.
# content of pytest.toml
[pytest]
usefixtures = ["cleandir"]
Warnung
Beachten Sie, dass diese Markierung in Fixture-Funktionen keine Auswirkung hat. Zum Beispiel funktioniert dies nicht wie erwartet.
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
Dies erzeugt eine Deprecation-Warnung und wird in Pytest 8 zu einem Fehler.
Überschreiben von Fixtures auf verschiedenen Ebenen¶
In einer relativ großen Testsuite müssen Sie höchstwahrscheinlich ein globales oder root-Fixture mit einem lokal definierten überschreiben, um den Testcode lesbar und wartbar zu halten.
Überschreiben eines Fixtures auf Ordner- (conftest) Ebene¶
Gegeben die Testdateistruktur ist.
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
conftest.py
# content of tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something_else.py
# content of tests/subfolder/test_something_else.py
def test_username(username):
assert username == 'overridden-username'
Wie Sie sehen, kann ein Fixture mit demselben Namen für eine bestimmte Testordner-Ebene überschrieben werden. Beachten Sie, dass auf das base- oder super-Fixture aus dem overriding-Fixture leicht zugegriffen werden kann – wie im obigen Beispiel verwendet.
Überschreiben eines Fixtures auf Testmodul-Ebene¶
Gegeben die Testdateistruktur ist.
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
Im obigen Beispiel kann ein Fixture mit demselben Namen für ein bestimmtes Testmodul überschrieben werden.
Überschreiben eines Fixtures mit direkter Testparametrisierung¶
Gegeben die Testdateistruktur ist.
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# content of tests/test_something.py
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
Im obigen Beispiel wird der Wert eines Fixtures durch den Testparameterwert überschrieben. Beachten Sie, dass der Wert des Fixtures auf diese Weise überschrieben werden kann, auch wenn der Test ihn nicht direkt verwendet (ihn nicht im Funktionsprototyp erwähnt).
Überschreiben eines parametrisierten Fixtures mit einem nicht-parametrisierten und umgekehrt¶
Gegeben die Testdateistruktur ist.
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
Im obigen Beispiel wird ein parametrisiertes Fixture durch eine nicht-parametrisierte Version überschrieben, und ein nicht-parametrisiertes Fixture wird für ein bestimmtes Testmodul durch eine parametrisierte Version überschrieben. Dasselbe gilt offensichtlich auch für die Testordner-Ebene.
Verwenden von Fixtures aus anderen Projekten¶
Normalerweise verwenden Projekte, die pytest-Unterstützung bieten, Entry Points, sodass die Installation dieser Projekte in einer Umgebung diese Fixtures verfügbar macht.
Wenn Sie Fixtures aus einem Projekt verwenden möchten, das keine Entry Points verwendet, können Sie pytest_plugins in Ihrer obersten conftest.py-Datei definieren, um dieses Modul als Plugin zu registrieren.
Angenommen, Sie haben einige Fixtures in mylibrary.fixtures und möchten sie in Ihrem app/tests-Verzeichnis wiederverwenden.
Alles, was Sie tun müssen, ist pytest_plugins in app/tests/conftest.py zu definieren und auf dieses Modul zu verweisen.
pytest_plugins = "mylibrary.fixtures"
Dies registriert effektiv mylibrary.fixtures als Plugin und macht alle seine Fixtures und Hooks für Tests in app/tests verfügbar.
Hinweis
Manchmal importieren Benutzer Fixtures aus anderen Projekten zur Verwendung, dies wird jedoch nicht empfohlen: Das Importieren von Fixtures in ein Modul registriert sie in pytest als in diesem Modul *definiert*.
Dies hat geringfügige Konsequenzen, wie z. B. das mehrmalige Erscheinen in pytest --help, wird aber **nicht empfohlen**, da sich dieses Verhalten in zukünftigen Versionen ändern/nicht mehr funktionieren könnte.