Python-Entwickler greifen zu pytest, weil es API-Tests ohne viel Framework-Code ermöglicht: Ein Test ist eine Funktion mit test_, Assertions sind normale assert-Anweisungen, und requests übernimmt HTTP. Zusammen ergeben beide Bibliotheken eine schlanke, code-zentrierte Basis für automatisierte API-Tests.
Probieren Sie Apidog noch heute aus
In diesem Tutorial bauen Sie eine praktische pytest-API-Testsuite auf: Projektstruktur, erster Request-Test, wiederverwendbare Fixtures, parametrisierte Testfälle, Assertions für Statuscode und Body sowie JSON-Schema-Validierung. Die Beispiele verwenden eine realistische REST-API-Struktur und lassen sich direkt auf eigene Endpunkte übertragen.
Projekt einrichten
Erstellen Sie zuerst eine virtuelle Umgebung und installieren Sie die benötigten Pakete:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Eine einfache, wartbare Struktur sieht so aus:
api-tests/
conftest.py # gemeinsame Fixtures und Konfiguration
test_users.py # Tests für User-Endpunkte
test_orders.py # Tests für Order-Endpunkte
pytest.ini # pytest-Konfiguration
Pytest erkennt Tests automatisch:
- Dateien beginnen mit
test_oder enden mit_test.py - Testfunktionen beginnen mit
test_ - Testklassen beginnen mit
Testund haben keine__init__-Methode
Beispiel für pytest.ini:
[pytest]
testpaths = .
python_files = test_*.py *_test.py
addopts = -v
markers =
smoke: schnelle Smoke-Tests
slow: langsamere Integrations- oder End-to-End-Tests
Wenn automatisiertes Testen für Sie neu ist, liefert der Überblick zu was automatisiertes Testen ist den nötigen Kontext.
Ersten API-Test schreiben
Ein API-Test sendet eine Anfrage und prüft die Antwort:
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"
Ausführen:
pytest -v
Pytest zeigt bei fehlgeschlagenen assert-Anweisungen den tatsächlichen Wert und die fehlerhafte Bedingung an. Sie brauchen keine speziellen Assertion-Methoden.
Für typische Prüfungen bei API-Antworten siehe auch den Leitfaden zu API-Assertions.
Setup mit Fixtures teilen
Basis-URL, HTTP-Session und Authentifizierung sollten nicht in jedem Test dupliziert werden. Verwenden Sie Fixtures in conftest.py.
# conftest.py
import os
import pytest
import requests
@pytest.fixture(scope="session")
def base_url():
return os.environ.get("API_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, base_url):
response = api_session.post(
f"{base_url}/auth/login",
json={
"email": "qa@example.com",
"password": "test-pass",
},
)
assert response.status_code == 200
return response.json()["token"]
Wichtige Punkte:
-
scope="session"erstellt die Ressource einmal pro Testlauf. -
yieldtrennt Setup und Teardown. - Tests fordern Fixtures über Funktionsparameter an.
-
base_urlkommt aus einer Umgebungsvariable und ist damit CI-tauglich.
Ein Test kann die Fixtures direkt verwenden:
def test_create_order(api_session, base_url, 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 response.status_code == 201
assert body["status"] == "pending"
Fixtures sind der moderne Ersatz für setup_function und teardown_function. Sie lassen sich kombinieren, haben definierte Scopes und machen Abhängigkeiten explizit. Details stehen in der offiziellen pytest Fixtures-Dokumentation.
Einen Test mit vielen Eingaben ausführen
API-Endpunkte müssen mit gültigen Werten, ungültigen Werten und Grenzfällen getestet werden. Dafür eignet sich @pytest.mark.parametrize.
import pytest
@pytest.mark.parametrize(
"user_id, expected_status",
[
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
],
)
def test_get_user_status_codes(api_session, base_url, user_id, expected_status):
response = api_session.get(f"{base_url}/users/{user_id}")
assert response.status_code == expected_status
Pytest erzeugt daraus vier separate Testfälle. Jeder Fall läuft unabhängig und erscheint einzeln im Report.
Bei größeren Datenmengen können Sie Eingaben aus CSV oder JSON laden. Der Leitfaden zu datengesteuerten API-Tests mit CSV und JSON beschreibt dieses Muster. Für erwartete Statuscodes ist die Referenz zu HTTP-Statuscodes, die REST-APIs verwenden sollten hilfreich.
Antwort-Body prüfen
Statuscodes reichen nicht aus. Eine 200-Antwort mit falschem Body ist weiterhin ein Fehler.
def test_order_response_shape(api_session, base_url, 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 response.status_code == 201
assert isinstance(body["id"], int)
assert body["product_id"] == 7
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
Praktische Checks für API-Antworten:
status_code- Pflichtfelder im JSON
- Datentypen
- fachliche Werte
- Fehlerstruktur bei
4xx - grobe Antwortzeitgrenzen
Zeit-Assertions sollten großzügig sein, damit normale Netzwerkvarianz keine instabilen Tests erzeugt.
JSON-Schema validieren
Für stabile Verträge zwischen Client und API validieren Sie die Antwort gegen ein JSON-Schema.
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, base_url, 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
validate(instance=response.json(), schema=order_schema)
Schema-Validierung findet strukturelle Probleme wie fehlende, umbenannte oder falsch typisierte Felder. Die jsonschema-Bibliothek ist dafür eine gängige Wahl; die unterstützten Schlüsselwörter stehen in der Validierungsdokumentation.
Fehlerfälle testen
Positive Tests prüfen, ob die API funktioniert. Negative Tests prüfen, ob sie korrekt ablehnt.
@pytest.mark.parametrize(
"payload, expected_status",
[
({}, 400),
({"product_id": 7}, 400),
({"quantity": 2}, 400),
({"product_id": 7, "quantity": 0}, 400),
],
)
def test_create_order_rejects_invalid_payloads(
api_session,
base_url,
auth_token,
payload,
expected_status,
):
response = api_session.post(
f"{base_url}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json=payload,
)
assert response.status_code == expected_status
Wenn Ihre API standardisierte Fehlerantworten zurückgibt, prüfen Sie diese ebenfalls:
def test_create_order_returns_error_body(api_session, base_url, auth_token):
response = api_session.post(
f"{base_url}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 0},
)
body = response.json()
assert response.status_code == 400
assert "error" in body
assert "message" in body["error"]
Suite in CI ausführen
Pytest gibt bei Fehlern einen Exit-Code ungleich Null zurück. Das passt direkt zu CI-Systemen.
pytest -v --junitxml=results.xml
Der JUnit-Report kann in GitHub Actions, GitLab CI, Jenkins oder anderen Pipelines angezeigt werden.
Beispiel für GitHub Actions:
name: API tests
on:
push:
pull_request:
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest requests jsonschema
- name: Run pytest
env:
API_BASE_URL: ${{ secrets.API_BASE_URL }}
run: |
pytest -v --junitxml=results.xml
Der Leitfaden zu API-Tests in CI/CD-Pipelines zeigt das vollständige Setup, einschließlich Secrets und Umgebungsauswahl.
Konfiguration nicht hardcodieren
Hardcodieren Sie keine Secrets, Tokens oder Umgebungs-URLs in Testdateien. Verwenden Sie Umgebungsvariablen:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
API_TOKEN = os.environ["API_TOKEN"]
Für lokale Entwicklung können Sie die Variablen vor dem Testlauf setzen:
export API_BASE_URL="https://staging.example.com/v1"
export API_TOKEN="local-token"
pytest -v
In CI kommen diese Werte aus Secrets.
Tests parallel ausführen
Für schnelleres Feedback können unabhängige Tests parallel laufen. Installieren Sie pytest-xdist:
pip install pytest-xdist
Dann ausführen:
pytest -n auto
Parallelisierung funktioniert nur zuverlässig, wenn Tests keinen geteilten Zustand verwenden. Jeder Test sollte eigene Daten erstellen oder eindeutig isolierte Ressourcen verwenden. Eine Suite, die von der Ausführungsreihenfolge abhängt, wird bei paralleler Ausführung instabil.
Suite wartbar halten
Eine kleine Suite ist einfach. Eine große Suite braucht Struktur.
Empfohlene Regeln:
- Tests nach Domäne gruppieren
test_users.py
test_orders.py
test_payments.py
- Fixtures zentralisieren
Wiederverwendbare Sessions, Tokens, Testdaten und Cleanup-Logik gehören nach conftest.py.
- Marks verwenden
import pytest
@pytest.mark.smoke
def test_health_check(api_session, base_url):
response = api_session.get(f"{base_url}/health")
assert response.status_code == 200
Ausführen:
pytest -m smoke
- Duplikate extrahieren
Wenn Sie denselben Request-Aufbau mehrfach schreiben, erstellen Sie eine Hilfsfunktion oder ein Fixture.
- Daten und Assertions trennen
Testdaten, Schemas und Hilfsfunktionen sollten nicht über alle Testdateien verstreut sein.
Diese modulare Disziplin gilt für jedes Testframework. Der Leitfaden zum Schreiben automatisierter Testskripte behandelt dieselbe Grundidee.
Wann eine Plattform sinnvoller ist
Ein pytest-Framework ist stark, wenn Ihr Team Python nutzt und Tests direkt neben dem Anwendungscode pflegen möchte. Es wird weniger praktisch, wenn QA- oder Produktteams ohne Python-Code beitragen sollen oder wenn Testdesign, Mocking und Ausführung an einem Ort liegen sollen.
Apidog adressiert diesen Fall mit visuellem Testdesign, Schema-Validierung gegen OpenAPI-Spezifikationen, datengesteuerten Läufen aus CSV und JSON sowie einem CLI-Runner für CI. Viele Teams kombinieren beides: pytest für logikintensive Szenarien und Apidog für breite API-Abdeckung, Design und Mocking. Sie können Apidog herunterladen und beide Ansätze an einem echten Endpunkt vergleichen.
Häufig gestellte Fragen
Warum pytest statt Pythons eingebautem unittest für API-Tests verwenden?
Pytest benötigt weniger Boilerplate-Code. Tests sind normale Funktionen, Assertions sind einfache assert-Anweisungen, und Fixtures sind flexibler als klassenbasierte setUp- und tearDown-Methoden. Außerdem bietet pytest ein großes Plugin-Ökosystem und integriertes parametrize für datengesteuerte Tests.
Was ist der Unterschied zwischen Fixture und parametrize?
Ein Fixture stellt wiederverwendbares Setup bereit, zum Beispiel eine HTTP-Session oder ein Auth-Token. parametrize führt denselben Test mehrfach mit unterschiedlichen Eingabewerten aus. Fixtures teilen Ressourcen; parametrize erzeugt mehrere Testfälle.
Sollte ich Antwortzeiten in pytest prüfen?
Ja, mit response.elapsed.total_seconds(). Nutzen Sie aber großzügige Grenzwerte. Pytest ist für funktionale Tests gedacht, nicht für Lasttests. Für echte Performance-Tests sollten Sie ein spezialisiertes Tool verwenden.
Wie halte ich API-Tests unabhängig?
Jeder Test sollte eigene Daten erstellen oder isolierte Ressourcen verwenden. Verlassen Sie sich nicht auf die Ausführungsreihenfolge. Unabhängige Tests lassen sich parallel ausführen und sind einfacher zu debuggen.
Kann pytest Antworten gegen eine OpenAPI-Spezifikation validieren?
Pytest selbst nicht direkt. Sie können mit jsonschema gegen JSON-Schemas validieren oder Plugins einsetzen, die OpenAPI-Dokumente prüfen. Wenn Schema-Validierung zentral für Ihren Workflow ist, kann eine Plattform wie Apidog das Setup vereinfachen.
Top comments (0)