Python-Entwickler greifen zu pytest, weil es sich nicht in den Weg stellt. Ein Test ist einfach eine Funktion, deren Name mit test_ beginnt, eine Assertion ist eine einfache assert-Anweisung, und der Runner erledigt den Rest. Kombiniert man es mit der requests-Bibliothek, erhält man ein vollständiges, code-zentriertes Framework zur Automatisierung von API-Tests, ohne übermäßigen Aufwand.
Dieses Tutorial zeigt, wie man eine echte pytest API-Testsuite aufbaut. Sie werden das Projekt einrichten, Ihren ersten Anforderungstest schreiben, Setup-Logik mit Fixtures teilen, denselben Test mit vielen Eingaben mittels parametrize ausführen und Assertions gegen den Antwortstatus, den Body und das JSON-Schema durchführen. Jedes Beispiel verwendet eine realistische API im öffentlichen Stil, sodass Sie den Code direkt anpassen können.
Projekt einrichten
Installieren Sie die beiden benötigten Bibliotheken in einer virtuellen Umgebung:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Ein übersichtliches Layout sorgt dafür, dass die Suite auch bei wachsender Größe wartbar bleibt:
api-tests/
conftest.py # shared fixtures
test_users.py # tests for the users endpoints
test_orders.py # tests for the orders endpoints
pytest.ini # configuration
Pytest entdeckt Tests automatisch. Dateien müssen mit test_ beginnen oder mit _test.py enden, Funktionen müssen mit test_ beginnen, und Testklassen müssen mit Test beginnen und keine __init__ Methode haben. Befolgen Sie diese Regeln, und Sie müssen die Test-Entdeckung nie manuell konfigurieren. Wenn automatisiertes Testen als Disziplin neu für Sie ist, bietet unser Überblick über was automatisiertes Testen ist den nötigen Kontext.
Warum pytest speziell für API-Tests? Die requests-Bibliothek kümmert sich um HTTP, und pytest erledigt alles darum herum: Entdeckung, Assertions mit lesbarer Fehlerausgabe, Setup und Teardown durch Fixtures, datengesteuerte Läufe mittels parametrize und Reporting. Sie stellen ein vollständiges API-Automatisierungsframework aus zwei kleinen, gut dokumentierten Bibliotheken zusammen, in einer Sprache, die Ihr Team wahrscheinlich bereits für die Anwendung selbst verwendet. Diese Nähe ist wichtig. Tests, die im selben Repository wie der Code liegen, bleiben ehrlich, weil eine bahnbrechende Änderung und ihr fehlschlagender Test im selben Pull Request auftauchen.
Ihren ersten API-Test schreiben
Ein pytest API-Test sendet eine Anfrage und überprüft die Antwort. Hier ist ein Test gegen einen Benutzer-Endpunkt:
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
Führen Sie die Suite mit pytest -v aus. Jedes fehlgeschlagene assert erzeugt einen detaillierten Bericht, der den tatsächlichen Wert anzeigt, was eine der besten Funktionen von pytest ist. Sie benötigen keine speziellen Assertionsmethoden; das Framework schreibt einfache assert-Anweisungen um, um eine umfangreiche Ausgabe zu liefern. Für die umfassenderen Überprüfungen, die bei einer Antwort sinnvoll sind, siehe unseren Leitfaden zu API-Assertions.
Setup mit Fixtures teilen
Die Wiederholung der Basis-URL und einer HTTP-Sitzung in jedem Test ist verschwenderisch. Fixtures lösen dieses Problem. Ein Fixture ist eine Funktion, die mit @pytest.fixture dekoriert ist und einen Wert erzeugt, den Tests anfordern können, indem sie ihn als Parameter benennen.
Legen Sie geteilte Fixtures in conftest.py ab, damit jede Testdatei sie ohne Importieren verwenden kann:
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={"email": "qa@example.com", "password": "test-pass"},
)
return response.json()["token"]
Das Argument scope="session" bedeutet, dass die Sitzung einmal für den gesamten Lauf und nicht pro Test erstellt wird. Das Schlüsselwort yield trennt Setup von Teardown: Code vor yield wird zuerst ausgeführt, Code danach wird ausgeführt, wenn das Fixture seinen Gültigkeitsbereich verlässt. Ein Test fordert einfach an, was er braucht:
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
Fixtures sind der moderne Ersatz von pytest für den älteren setup_function- und teardown_function-Stil. Sie lassen sich sauber zusammensetzen, unterstützen Scopes und machen Abhängigkeiten explizit, weshalb die offizielle pytest Fixtures-Dokumentation sie als Standardansatz empfiehlt.
Einen Test mit vielen Eingaben ausführen
API-Endpunkte müssen normalerweise mit vielen Eingaben überprüft werden: gültige Werte, ungültige Werte und Grenzfälle. Für jeden Fall eine separate Funktion zu schreiben, ist mühsam. Der @pytest.mark.parametrize Decorator führt einen Testkörper mit einer Liste von Eingaben aus:
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize("user_id,expected_status", [
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
Dies erzeugt vier separate Testfälle aus einer Funktion. Jeder läuft und berichtet unabhängig, sodass eine einzelne schlechte Eingabe die anderen nicht verbirgt. Parametrize ist die integrierte Antwort von pytest auf datengesteuerte Tests. Wenn die Eingabemenge groß wird, laden Sie sie stattdessen aus einer Datei; unser Leitfaden zu datengesteuerten API-Tests mit CSV und JSON behandelt dieses Muster. Wenn Sie unsicher sind, welchen Statuscode jede Eingabe zurückgeben sollte, ist die Referenz zu HTTP-Statuscodes, die REST-APIs verwenden sollten ein nützlicher Begleiter.
Assertions für den Antwort-Body und das Schema
Statuscodes sind notwendig, aber nicht ausreichend. Eine 200er-Antwort mit einem fehlerhaften Body ist immer noch ein Fehler. Überprüfen Sie das geparste JSON direkt:
def test_order_response_shape(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert isinstance(body["id"], int)
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
Für stärkere Garantien validieren Sie den Body gegen ein JSON-Schema. Dies fängt strukturelle Abweichungen ab, wie z.B. ein umbenanntes oder fehlendes Feld, die manuell geschriebene Prüfungen übersehen würden:
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
validate(instance=response.json(), schema=order_schema)
Die Schema-Validierung skaliert besser als feldweise Assertions, da ein Schema die gesamte Antwortstruktur abdeckt. Die jsonschema-Bibliothek ist die Standardwahl, und ihre Validierungsdokumentation erklärt die unterstützten Schlüsselwörter.
Die Suite in CI ausführen
Eine pytest-Suite bewährt sich, wenn sie automatisch läuft. Pytest gibt bei einem Fehler einen Exit-Code ungleich Null zurück, was genau das ist, was ein CI-Server benötigt, um einen Build fehlschlagen zu lassen. Geben Sie einen JUnit-Bericht für die Inline-Anzeige aus:
pytest -v --junitxml=results.xml
Binden Sie diesen Befehl in einen GitHub Actions Schritt oder eine andere Pipeline ein, und Ihre API-Tests sichern jeden Commit. Unser Leitfaden zu API-Tests in CI/CD-Pipelines zeigt das vollständige Setup, einschließlich der Injektion von Secrets für Token und der Umgebungsauswahl.
Zwei CI-Gewohnheiten halten eine pytest-Suite zuverlässig. Erstens, hardcodieren Sie niemals Secrets oder Umgebungs-URLs in Testdateien. Lesen Sie diese aus Umgebungsvariablen, damit dieselbe Suite im CI gegen Staging und lokal ohne Änderungen ausgeführt werden kann:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Zweitens, führen Sie unabhängige Tests parallel aus, um schnelles Feedback zu gewährleisten. Das pytest-xdist-Plugin verteilt Tests mit pytest -n auto auf CPU-Kerne. Parallele Läufe funktionieren nur, wenn Ihre Tests keinen Zustand teilen, was ein weiterer Grund ist, warum die Testdatenschicht wichtig ist. Eine Suite, die von der Ausführungsreihenfolge abhängt, wird unvorhersehbar fehlschlagen, sobald sie parallel ausgeführt wird.
Eine pytest-Suite wartbar halten
Eine Suite von fünfzig Tests ist einfach. Eine Suite von fünfhundert erfordert Disziplin. Drei Praktiken halten ein pytest API-Framework gesund, wenn es wächst.
Gruppieren Sie verwandte Tests in Modulen und verwenden Sie Klassen nur, wenn sie ein gemeinsames Setup haben, nicht zur Dekoration. Eine test_orders.py-Datei mit einem klaren Satz von Funktionen ist besser lesbar als eine riesige Datei. Verwenden Sie in pytest.ini registrierte Marks, um Tests zu kennzeichnen, damit Sie Untergruppen ausführen können: @pytest.mark.smoke für eine schnelle Überprüfung, @pytest.mark.slow für den vollständigen Durchlauf. Führen Sie den Smoke-Test bei jedem Commit aus und den vollständigen Satz jede Nacht.
Konfiguration zentralisieren. Basis-URLs, Schemas und geteilte Fixtures gehören in conftest.py oder ein kleines Konfigurationsmodul, niemals quer durch Dateien kopieren und einfügen. Wenn sich die Staging-URL ändert, sollten Sie eine Zeile bearbeiten. Die gleiche modulare Disziplin, die für jedes Framework gilt und in unserem Leitfaden zum Schreiben automatisierter Testskripte behandelt wird, gilt auch hier: Extrahieren Sie alles, was Sie zweimal schreiben, in ein Fixture oder eine Hilfsfunktion.
Wann man stattdessen zu einer Plattform greifen sollte
Ein pytest-Framework ist ausgezeichnet, wenn Ihr Team Python schreibt und Tests neben dem Anwendungscode haben möchte. Es ist weniger praktisch, wenn QA- oder Produktmitarbeiter beitragen müssen oder wenn Sie Testdesign, Mocking und Ausführung an einem Ort wünschen, ohne Kleber-Code zu pflegen.
Apidog schließt diese Lücke. Es bietet visuelles Testdesign, Schema-Validierung gegen Ihre OpenAPI-Spezifikation, datengesteuerte Ausführungen aus CSV und JSON sowie einen CLI-Runner für CI, alles ohne das manuelle Schreiben von Fixture- und Assertionscode. Viele Teams verwenden beides: pytest für logikintensive Szenarien und Apidog für eine breite Abdeckung sowie für das Design und das Mocking der APIs, gegen die die pytest-Suite testet. Sie können Apidog herunterladen und die beiden Ansätze an einem echten Endpunkt an einem Nachmittag vergleichen.
Häufig gestellte Fragen
Warum pytest anstelle von Pythons eingebautem unittest für API-Tests verwenden?
Pytest benötigt weniger Boilerplate-Code. Tests sind einfache Funktionen, Assertions sind einfache assert-Anweisungen mit umfangreicher Fehlerausgabe, und Fixtures handhaben das Setup flexibler als die klassenbasierten Methoden von unittest. Pytest verfügt außerdem über ein großes Plugin-Ökosystem und ein integriertes parametrize für datengesteuerte Tests. Es kann weiterhin bestehende Tests im unittest-Stil ausführen, sodass die Migration ein geringes Risiko darstellt.
Was ist der Unterschied zwischen einem Fixture und parametrize?
Ein Fixture stellt eine wiederverwendbare Ressource, wie eine HTTP-Sitzung oder ein Auth-Token, für jeden Test bereit, der sie anfordert. Parametrize führt denselben Testkörper mehrmals mit verschiedenen Eingabewerten aus. Fixtures teilen das Setup; parametrize vervielfacht die Fälle. Sie lassen sich gut kombinieren: ein parametrisierter Test kann weiterhin von Fixtures abhängen.
Sollte ich die Antwortzeit in pytest API-Tests überprüfen?
Ja, das können Sie, indem Sie response.elapsed.total_seconds() verwenden, und eine lockere Obergrenze fängt grobe Regressionen ab. Aber pytest ist ein Tool für funktionale Tests, kein Lasttest-Tool. Für echte Performance-Arbeit verwenden Sie ein spezielles Tool. Halten Sie Zeit-Assertions großzügig, damit normale Netzwerkvarianzen keine instabilen Fehler verursachen.
Wie halte ich API-Tests in pytest unabhängig?
Geben Sie jedem Test eigene Daten durch Fixtures, die Ressourcen erstellen und bereinigen, und vermeiden Sie es, sich auf die Testausführungsreihenfolge zu verlassen. Pytest führt Tests standardmäßig in Dateireihenfolge aus, aber eine gut konzipierte Suite ist nicht davon abhängig. Unabhängige Tests können parallel ausgeführt werden und isoliert fehlschlagen, was das Debugging erheblich erleichtert.
Kann pytest Antworten gegen eine OpenAPI-Spezifikation validieren?
Pytest selbst nicht, aber Sie können mit der jsonschema-Bibliothek gegen ein JSON-Schema validieren, und es gibt Plugins, die Antworten gegen ein OpenAPI-Dokument prüfen. Wenn die Schema-Validierung für Ihren Workflow zentral ist, kann eine Plattform wie Apidog, die automatisch gegen Ihre OpenAPI-Spezifikation validiert, Ihnen das Plugin-Setup ersparen.
