API-Tests mit Django REST Framework & pytest
REST-APIs sind Verträge: Clients verlassen sich auf Statuscodes, Feldnamen und Verhalten. Genau deshalb gehören sie umfassend getestet. Mit DRF und pytest geht das erstaunlich elegant — hier die Muster, die ich in jedem API-Projekt nutze.
Der APIClient
DRF bringt einen eigenen Test-Client mit, der JSON und Authentifizierung sauber unterstützt:
# conftest.py
import pytest
from rest_framework.test import APIClient
@pytest.fixture
def api():
return APIClient()
@pytest.fixture
def auth_api(api, user):
api.force_authenticate(user=user)
return apiCRUD-Endpunkte testen
@pytest.mark.django_db
class TestArticleAPI:
def test_liste(self, api):
ArticleFactory.create_batch(3, status="published")
response = api.get("/api/articles/")
assert response.status_code == 200
assert len(response.data["results"]) == 3
def test_detail(self, api):
article = ArticleFactory(status="published")
response = api.get(f"/api/articles/{article.id}/")
assert response.status_code == 200
assert response.data["title"] == article.title
def test_erstellen(self, auth_api):
payload = {"title": "Neu", "status": "draft"}
response = auth_api.post("/api/articles/", payload, format="json")
assert response.status_code == 201
assert Article.objects.filter(title="Neu").exists()Authentifizierung
@pytest.mark.django_db
def test_erstellen_ohne_auth_verboten(api):
response = api.post("/api/articles/", {"title": "X"}, format="json")
assert response.status_code == 401 # nicht authentifiziert
@pytest.mark.django_db
def test_token_auth(api, user):
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)
api.credentials(HTTP_AUTHORIZATION=f"Token {token.key}")
response = api.get("/api/articles/")
assert response.status_code == 200Permissions prüfen
Ein häufig übersehener Testfall: Darf ein Nutzer nur seine eigenen Objekte ändern?
@pytest.mark.django_db
def test_fremden_artikel_nicht_aendern(api):
autor_a = UserFactory()
autor_b = UserFactory()
artikel = ArticleFactory(author=autor_a)
api.force_authenticate(user=autor_b) # anderer Nutzer
response = api.patch(f"/api/articles/{artikel.id}/",
{"title": "gekapert"}, format="json")
assert response.status_code == 403 # verbotenSerializer-Validierung
@pytest.mark.django_db
def test_ungueltige_daten(auth_api):
# title fehlt (Pflichtfeld)
response = auth_api.post("/api/articles/", {"status": "draft"}, format="json")
assert response.status_code == 400
assert "title" in response.data
@pytest.mark.django_db
def test_ungueltiger_status(auth_api):
response = auth_api.post("/api/articles/",
{"title": "X", "status": "quatsch"}, format="json")
assert response.status_code == 400
assert "status" in response.dataFixtures für API-Tests
Wiederkehrende Setups bündele ich in Fixtures, damit die Tests schlank bleiben:
@pytest.fixture
def veroeffentlichte_artikel(db):
return ArticleFactory.create_batch(5, status="published")
@pytest.fixture
def admin_api(api):
admin = UserFactory(is_staff=True, is_superuser=True)
api.force_authenticate(user=admin)
return api
# Test bleibt lesbar:
def test_admin_sieht_entwuerfe(admin_api, veroeffentlichte_artikel):
ArticleFactory(status="draft")
response = admin_api.get("/api/articles/?status=draft")
assert len(response.data["results"]) == 1Fazit
Gute API-Tests decken mehr ab als "gibt 200 zurück": Statuscodes, Auth, Permissions (besonders Fremd-Zugriff!), Validierung und Randfälle. Mit DRFs APIClient, pytest-Fixtures und Factory Boy bleiben sie dabei schlank und lesbar. So wird die API zum verlässlichen Vertrag — auch nach dem nächsten Refactoring.
