DEV Community

Cover image for Pytest API Automatisierung: Framework & Tutorial
Emre Demir
Emre Demir

Posted on • Originally published at apidog.com

Pytest API Automatisierung: Framework & Tutorial

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pytest erkennt Tests automatisch:

  • Dateien beginnen mit test_ oder enden mit _test.py
  • Testfunktionen beginnen mit test_
  • Testklassen beginnen mit Test und 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Ausführen:

pytest -v
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Wichtige Punkte:

  • scope="session" erstellt die Ressource einmal pro Testlauf.
  • yield trennt Setup und Teardown.
  • Tests fordern Fixtures über Funktionsparameter an.
  • base_url kommt 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Dann ausführen:

pytest -n auto
Enter fullscreen mode Exit fullscreen mode

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:

  1. Tests nach Domäne gruppieren
   test_users.py
   test_orders.py
   test_payments.py
Enter fullscreen mode Exit fullscreen mode
  1. Fixtures zentralisieren

Wiederverwendbare Sessions, Tokens, Testdaten und Cleanup-Logik gehören nach conftest.py.

  1. 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
Enter fullscreen mode Exit fullscreen mode

Ausführen:

   pytest -m smoke
Enter fullscreen mode Exit fullscreen mode
  1. Duplikate extrahieren

Wenn Sie denselben Request-Aufbau mehrfach schreiben, erstellen Sie eine Hilfsfunktion oder ein Fixture.

  1. 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)