TDD und BDD für APIs: Wenn deine Web Apps miteinander reden müssen
Hier ist ein Szenario das jedem bekannt vorkommen wird der an verteilten Systemen gearbeitet hat: du hast zwei Webanwendungen die miteinander kommunizieren müssen. Vielleicht ist es eine Frontend App die mit einer Backend API redet. Vielleicht ist es eine Hauptplattform die sich mit einem Zahlungsservice integriert. Vielleicht ist es ein Mobile App Backend das sich mit einem Lagerverwaltungssystem koordiniert. Egal welches Setup, du hast zwei separate Codebasen, potenziell zwei separate Teams, und einen API Contract der dazwischen sitzt. Und wenn dieser Contract bricht, fällt alles auseinander.
Hier ist ein Szenario das jedem bekannt vorkommen wird der an verteilten Systemen gearbeitet hat: du hast zwei Webanwendungen die miteinander kommunizieren müssen. Vielleicht ist es eine Frontend App die mit einer Backend API redet. Vielleicht ist es eine Hauptplattform die sich mit einem Zahlungsservice integriert. Vielleicht ist es ein Mobile App Backend das sich mit einem Lagerverwaltungssystem koordiniert. Egal welches Setup, du hast zwei separate Codebasen, potenziell zwei separate Teams, und einen API Contract der dazwischen sitzt. Und wenn dieser Contract bricht, fällt alles auseinander.
Ich habe Jahre damit verbracht Systeme zu bauen wo mehrere Anwendungen durch APIs koordinieren müssen. Die Patterns die ich gleich teile kommen von echten Produktionssystemen, echten Ausfällen, und echten Lektionen die auf die harte Tour gelernt wurden. Am Ende dieses Posts wirst du verstehen nicht nur wie man APIs zwischen Anwendungen testet, sondern warum bestimmte Ansätze besser funktionieren als andere und welche Fallen du vermeiden solltest.
Das Problem: Zwei Apps, Ein Contract, Null Garantien
Lass uns damit anfangen zu verstehen warum API Testing zwischen separaten Anwendungen fundamental anders ist als Testing innerhalb einer einzelnen Anwendung.
Wenn du eine Rails oder Django App isoliert testest, kontrollierst du alles. Deine Models, Controller, Views und Tests leben alle in derselben Codebase. Wenn du etwas änderst, sagen dir deine Tests sofort ob du was kaputt gemacht hast.
Aber wenn App A die API von App B aufruft, hast du eine komplett andere Situation:
Separate Deployment Zyklen. App A deployt vielleicht am Montag, App B am Donnerstag. Was am Montag funktioniert hat könnte am Donnerstag brechen wenn App B seine API geändert hat.
Separate Teams. Das Team das App A baut weiß vielleicht nicht mal dass das Team das App B baut etwas geändert hat. Kommunikation passiert durch Slack Nachrichten, Emails, oder schlimmer, Produktionsfehler.
Separate Test Suites. App As Tests sind grün. App Bs Tests sind grün. Aber wenn sie in Produktion miteinander reden, explodiert alles.
Netzwerk Unzuverlässigkeit. Anders als Funktionsaufrufe innerhalb einer Anwendung können API Calls timeouts haben, teilweise fehlschlagen, oder unerwartete Fehler zurückgeben.
Versions Mismatches. App A ruft vielleicht einen Endpoint auf den App B vor drei Monaten deprecated hat aber nie wirklich entfernt hat.
Ich habe all diese Dinge Produktions Incidents verursachen sehen. Der schlimmste war eine Zahlungsintegration wo der Provider einen Feldnamen von transaction_id zu txn_id in ihrer Antwort geändert hat. Ihre Tests waren grün. Unsere Tests waren grün. Aber wir haben ihre Antworten basierend auf alter Dokumentation gemockt. Drei Tage fehlgeschlagene Zahlungen bevor es jemand bemerkt hat.
Die Denkschulen
Es gibt mehrere philosophische Ansätze zum Testen von APIs zwischen Anwendungen. Jeder hat Wert, und die meisten Produktionssysteme nutzen eine Kombination.
Schule 1: End to End Integration Testing
Der offensichtlichste Ansatz: beide Anwendungen hochfahren, sie aufeinander zeigen, und Tests laufen lassen die den vollen Flow ausführen.
Stärken:
Testet die echte Integration, nicht eine Simulation.
Fängt Probleme die Mocks verpassen würden.
Gibt hohes Vertrauen wenn Tests durchlaufen.
Schwächen:
Langsam. Richtig langsam. Du brauchst beide Anwendungen laufend, Datenbanken geseeded, potenziell externe Services gemockt.
Flaky. Netzwerkprobleme, Timing Probleme und Testdaten Verschmutzung verursachen intermittierende Fehler.
Teuer. Integrationsumgebungen zu pflegen ist signifikanter operativer Overhead.
Spätes Feedback. Du kannst diese oft nicht laufen lassen bis Code in eine Staging Umgebung deployed ist.
Wann nutzen:
Als finale Verifikationsschicht vor Produktions Deployment. Nicht als deine primäre Testing Strategie.
Schule 2: Alles Mocken
Das andere Extrem: jede Anwendung mockt die API der anderen komplett und testet isoliert.
Stärken:
Schnell. Keine Netzwerk Calls, keine externen Abhängigkeiten.
Zuverlässig. Keine Flakiness von externen Systemen.
Kann überall laufen, inklusive auf Entwickler Laptops ohne spezielles Setup.
Schwächen:
Mocks können von der Realität driften. Der Mock sagt die API gibt user_id zurück, aber die echte API gibt userId zurück. Deine Tests sind grün, Produktion bricht.
Falsches Vertrauen. Alles sieht grün aus, aber du testest gegen eine Fantasieversion der anderen Anwendung.
Wartungsaufwand. Jede API Änderung erfordert Updates an Mocks an mehreren Stellen.
Wann nutzen:
Für schnelle Entwicklung und Unit Testing, aber niemals als deine einzige Testing Strategie.
Schule 3: Contract Testing
Der Mittelweg: definiere einen Contract dem beide Seiten zustimmen, dann teste jede Seite gegen diesen Contract unabhängig.
Stärken:
Schnell wie Mocks, aber mit Garantien dass beide Seiten sich auf das Interface einigen.
Fängt Breaking Changes vor dem Deployment.
Funktioniert über Sprachgrenzen hinweg (eine Rails App kann einen Contract mit einer Django App haben).
Schwächen:
Lernkurve. Contract Testing erfordert das Verstehen neuer Konzepte und Tools.
Setup Overhead. Du brauchst Infrastruktur um Contracts zu teilen und zu verifizieren.
Testet nicht alles. Business Logik auf jeder Seite braucht trotzdem separates Testing.
Wann nutzen:
Als deine primäre Strategie für API Integration Testing. Ergänze mit selektiven End to End Tests.
Schule 4: Consumer Driven Contracts
Eine spezifische Variante von Contract Testing wo der Consumer (die App die die API aufruft) definiert was sie braucht, und der Provider (die App die die API bereitstellt) verifiziert dass sie diese Bedürfnisse erfüllen kann.
Stärken:
Consumer testen nur was sie tatsächlich nutzen. Wenn du nur drei Felder aus einer 50 Feld Antwort brauchst, verifizierst du nur diese drei.
Provider wissen genau was brechen würde wenn sie etwas ändern.
Dokumentiert natürlich welche Consumer von welchen Teilen der API abhängen.
Schwächen:
Erfordert Buy in von beiden Teams.
Contract Management wird eine Koordinations Herausforderung im großen Maßstab.
Kann Widerstand gegen API Evolution schaffen wenn Provider sich durch Consumer Contracts eingeschränkt fühlen.
Wann nutzen:
Wenn du klare Consumer/Provider Beziehungen hast und Prozesse für Contract Sharing etablieren kannst.
TDD für APIs: Der Rails Weg
Lass mich dir zeigen wie ich API Testing in Rails angehe, beginnend mit Test Driven Development für eine API die eine andere Anwendung konsumieren wird.
Deine API Tests aufsetzen
Rails hat exzellente eingebaute Unterstützung für API Testing durch Request Specs:
# spec/requests/api/v1/orders_spec.rb
require 'rails_helper'
RSpec.describe 'Orders API', type: :request do
describe 'GET /api/v1/orders/:id' do
let(:user) { create(:user) }
let(:order) { create(:order, user: user, total_cents: 4999) }
context 'with valid authentication' do
let(:headers) do
{
'Authorization' => "Bearer #{user.api_token}",
'Accept' => 'application/json'
}
end
it 'returns the order details' do
get "/api/v1/orders/#{order.id}", headers: headers
expect(response).to have_http_status(:ok)
expect(response.content_type).to include('application/json')
json = JSON.parse(response.body)
expect(json['id']).to eq(order.id)
expect(json['total_cents']).to eq(4999)
expect(json['status']).to eq('pending')
end
end
context 'without authentication' do
it 'returns unauthorized' do
get "/api/v1/orders/#{order.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
end
RSpec API Dokumentation mit rswag
Eins der mächtigsten Tools im Rails Ökosystem ist rswag, das OpenAPI (Swagger) Dokumentation aus deinen Tests generiert:
# spec/requests/api/v1/orders_swagger_spec.rb
require 'swagger_helper'
RSpec.describe 'Orders API', type: :request do
path '/api/v1/orders/{id}' do
get 'Retrieves an order' do
tags 'Orders'
produces 'application/json'
parameter name: :id, in: :path, type: :integer, description: 'Order ID'
parameter name: 'Authorization', in: :header, type: :string, required: true
response '200', 'order found' do
schema type: :object,
properties: {
id: { type: :integer },
total_cents: { type: :integer },
status: { type: :string }
},
required: ['id', 'total_cents', 'status']
let(:user) { create(:user) }
let(:id) { create(:order, user: user).id }
let(:Authorization) { "Bearer #{user.api_token}" }
run_test!
end
end
end
end
Führe rails rswag aus und du bekommst eine komplette OpenAPI Spezifikation die garantiert zu deinem echten API Verhalten passt weil sie aus Tests generiert wird die durchlaufen.
Stärken von rswag:
Dokumentation ist immer akkurat weil Tests durchlaufen müssen um sie zu generieren.
Generiert interaktive Swagger UI für API Exploration.
Schema Validierung stellt sicher dass Antworten zur dokumentierten Struktur passen.
Schwächen von rswag:
Verbose Test Syntax. Tests werden länger mit all den Schema Definitionen.
Lernkurve für OpenAPI Schema Spezifikation.
Kann Test Suite verlangsamen wenn übermäßig genutzt.
TDD für APIs: Der Django Weg
Django hat sein eigenes exzellentes Ökosystem für API Testing, zentriert um Django REST Framework (DRF).
Basis API Testing mit DRF
# tests/test_orders_api.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from orders.models import Order
from users.models import User
class OrderAPITestCase(APITestCase):
"""
Test Suite für die Orders API Endpoints.
Testet sowohl Happy Paths als auch Fehlerbedingungen.
"""
def setUp(self):
self.user = User.objects.create_user(
email='[email protected]',
password='testpass123'
)
self.client.force_authenticate(user=self.user)
def test_get_order_returns_order_details(self):
"""GET /api/v1/orders/:id gibt volle Order Details zurück"""
order = Order.objects.create(
user=self.user,
total_cents=4999,
status='pending'
)
url = reverse('order-detail', kwargs={'pk': order.id})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['id'], order.id)
self.assertEqual(response.data['total_cents'], 4999)
def test_get_order_without_auth_returns_401(self):
"""Unauthentifizierte Requests werden abgelehnt"""
self.client.force_authenticate(user=None)
order = Order.objects.create(user=self.user, total_cents=100)
url = reverse('order-detail', kwargs={'pk': order.id})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
drf-spectacular für Dokumentation
Das Django Äquivalent zu rswag ist drf-spectacular, das OpenAPI Dokumentation aus deinen DRF Views generiert:
# orders/views.py
from rest_framework import viewsets
from drf_spectacular.utils import extend_schema, OpenApiExample
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
@extend_schema(
summary="Retrieve an order",
description="Get detailed information about a specific order.",
responses={200: OrderSerializer, 404: OpenApiTypes.OBJECT},
examples=[
OpenApiExample(
'Successful retrieval',
value={'id': 123, 'total_cents': 4999, 'status': 'pending'}
)
]
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
Stärken von drf-spectacular:
Decorator basierter Ansatz hält Dokumentation nah am Code.
Automatische Schema Inferenz aus Serializers.
Exzellente Unterstützung für komplexe verschachtelte Strukturen.
Schwächen von drf-spectacular:
Dokumentation ist nicht garantiert zu Tests zu passen (anders als rswag).
Decorators können verbose werden für komplexe Endpoints.
Erfordert Disziplin um Dokumentation aktuell zu halten.
Contract Testing mit Pact
Jetzt lass uns über den mächtigsten Ansatz für das Testen von APIs zwischen separaten Anwendungen reden: Contract Testing mit Pact.
Pact ist ein Consumer Driven Contract Testing Framework das über Sprachen hinweg funktioniert. Dein Rails Consumer kann Contracts mit einem Django Provider verifizieren, oder umgekehrt.
Wie Pact funktioniert
- Der Consumer (die App die die API aufruft) schreibt Tests die beschreiben was sie vom Provider erwartet.
- Diese Tests generieren einen Contract (eine JSON Datei genannt Pact).
- Der Provider (die App die die API bereitstellt) führt den Contract gegen seine echte Implementation aus.
- Wenn der Provider alle Contracts erfüllt, weißt du dass die Integration funktionieren wird.
Consumer Seite (Rails App die externe API aufruft)
# spec/service_consumers/warehouse_api_consumer_spec.rb
require 'pact/consumer/rspec'
RSpec.describe WarehouseService, pact: true do
let(:warehouse_service) { WarehouseService.new(base_url: 'http://localhost:1234') }
describe 'create_shipment' do
before do
warehouse_api
.given('a product with SKU WIDGET001 exists')
.upon_receiving('a request to create a shipment')
.with(
method: :post,
path: '/api/v1/shipments',
body: {
order_id: Pact.like(12345),
items: Pact.each_like(sku: 'WIDGET001', quantity: 2)
}
)
.will_respond_with(
status: 201,
body: {
shipment_id: Pact.like('SHIP123456'),
status: 'pending'
}
)
end
it 'creates a shipment and returns tracking info' do
result = warehouse_service.create_shipment(
order_id: 12345,
items: [{ sku: 'WIDGET001', quantity: 2 }]
)
expect(result.shipment_id).to be_present
expect(result.status).to eq('pending')
end
end
end
Stärken von Pact:
Sprachagnostisch. Funktioniert über Ruby, Python, JavaScript, Go und mehr.
Fängt Breaking Changes vor dem Deployment.
Consumer Driven Ansatz stellt sicher dass du nur testest was tatsächlich genutzt wird.
Pact Broker bietet Contract Versionierung und Deployment Sicherheitschecks.
Schwächen von Pact:
Signifikante Lernkurve.
Erfordert Infrastruktur für Contract Sharing (Pact Broker).
Provider States können komplex zu managen werden.
Testet keine Business Logik, nur Interface Kompatibilität.
Die Gefahren: Was schief gehen kann
Lass mich einige warnende Geschichten über API Testing zwischen Anwendungen teilen.
Gefahr 1: Mock Drift
Du mockst die externe API in deinen Tests. Es funktioniert. Monate vergehen. Die externe API ändert sich. Deine Mocks nicht. Deine Tests sind immer noch grün, aber Produktion bricht.
Lösung: Nutze Contract Testing. Oder mindestens, zeichne regelmäßig echte API Antworten auf und vergleiche sie mit deinen Mocks.
Gefahr 2: Falsches Vertrauen durch Integration Tests
Deine Integration Tests sind grün in Staging. Du deployest. Produktion explodiert.
Warum? Weil Staging andere Daten hat, andere Last, andere Konfiguration. Der Integration Test hat bewiesen dass der Code funktionieren kann, nicht dass er funktionieren wird.
Lösung: Integration Tests sind notwendig aber nicht ausreichend. Kombiniere mit Contract Tests, umfassenden Unit Tests, und Produktions Monitoring.
Gefahr 3: Nur den Happy Path testen
Alle deine API Tests prüfen erfolgreiche Antworten. Aber was passiert wenn die API 500 zurückgibt? Oder timeout hat? Oder fehlerhaftes JSON zurückgibt?
# Das ist NICHT genug
it 'creates a shipment' do
stub_request(:post, 'https://api.warehouse.com/shipments')
.to_return(status: 200, body: { shipment_id: '123' }.to_json)
result = service.create_shipment(order)
expect(result).to be_success
end
# Du brauchst auch diese
it 'handles API errors gracefully' do
stub_request(:post, 'https://api.warehouse.com/shipments')
.to_return(status: 500, body: 'Internal Server Error')
result = service.create_shipment(order)
expect(result).to be_failure
end
it 'handles timeouts gracefully' do
stub_request(:post, 'https://api.warehouse.com/shipments')
.to_timeout
result = service.create_shipment(order)
expect(result).to be_failure
end
Gefahr 4: Gegen Produktions APIs testen
Ich habe Teams gesehen die Tests gegen echte Produktions APIs laufen lassen. Das ist aus mehreren Gründen gefährlich:
Du könntest echte Daten erstellen (Bestellungen, Belastungen, Sendungen).
Du bist abhängig von externer Service Verfügbarkeit für deine Tests.
Du könntest Rate Limits treffen.
Lösung: Nutze immer Sandboxes, Mocks, oder aufgezeichnete Antworten. Teste niemals gegen Produktions APIs.
API Tests als Dokumentation
Hier ist der Payoff für all diesen Testing Aufwand: deine API Tests werden die zuverlässigste Dokumentation die du hast.
Überleg mal: traditionelle API Dokumentation wird einmal geschrieben und fängt sofort an zu veralten. Jemand ändert einen Endpoint aber vergisst die Docs zu updaten. Feldnamen driften. Erforderliche Parameter werden optional. Die Dokumentation wird zu einer Belastung, die Entwickler aktiv in die Irre führt.
Aber Test basierte Dokumentation kann nicht lügen. Wenn der Test sagt der Endpoint gibt user_id zurück und er gibt tatsächlich userId zurück, schlägt der Test fehl. Die Dokumentation bleibt akkurat weil sie bei jedem Build verifiziert wird.
Dokumentation aus Tests generieren
In Rails mit rswag:
# OpenAPI Spec aus Tests generieren
bundle exec rails rswag:specs:swaggerize
# Interaktive Dokumentation bereitstellen
bundle exec rails s
# Besuche http://localhost:3000/api-docs
In Django mit drf-spectacular:
# OpenAPI Schema generieren
python manage.py spectacular --file schema.yml
Pact als Dokumentation nutzen
Pact Contracts dienen als Dokumentation von exakt dem was jeder Consumer braucht:
{
"consumer": { "name": "OrdersApp" },
"provider": { "name": "WarehouseAPI" },
"interactions": [
{
"description": "a request to create a shipment",
"request": {
"method": "POST",
"path": "/api/v1/shipments",
"body": {
"order_id": 12345,
"items": [{"sku": "WIDGET001", "quantity": 2}]
}
},
"response": {
"status": 201,
"body": {
"shipment_id": "SHIP123456",
"status": "pending"
}
}
}
]
}
Das sagt dir exakt was der Consumer sendet und was er zurück erwartet. Es ist präzise, eindeutig, und verifiziert.
Alles zusammenbringen: Eine Testing Strategie
Basierend auf Jahren an Erfahrung, hier ist mein empfohlener Ansatz für das Testen von APIs zwischen Anwendungen:
Schicht 1: Unit Tests Teste deine Serializers, Validators und Business Logik isoliert. Schnell, zuverlässig, laufen bei jedem Commit.
Schicht 2: Request/Controller Tests Teste deine API Endpoints mit echten HTTP Requests aber gemockten Abhängigkeiten. Verifiziere Authentication, Authorization, Validation und Response Formate.
Schicht 3: Contract Tests Nutze Pact oder Ähnliches um API Contracts zwischen Anwendungen zu verifizieren. Laufen bei jedem Commit. Blockiere Deployments wenn Contracts brechen.
Schicht 4: Integration Tests Selektive End to End Tests gegen Staging Umgebungen. Laufen vor großen Releases. Fokussiere auf kritische Pfade.
Schicht 5: Produktions Monitoring Monitore API Health, Fehlerraten und Antwortzeiten in Produktion. Alerte bei Anomalien. Das fängt was Tests nicht können.
Der Schlüssel ist dass jede Schicht verschiedene Arten von Problemen fängt. Unit Tests fangen Logikfehler. Contract Tests fangen Interface Mismatches. Integration Tests fangen Umgebungsprobleme. Produktions Monitoring fängt Real World Edge Cases.
Fazit
APIs zwischen separaten Webanwendungen zu testen ist fundamental anders als innerhalb einer einzelnen Anwendung zu testen. Die Herausforderungen separater Deployments, separater Teams und Netzwerk Unzuverlässigkeit verlangen spezifische Strategien.
Contract Testing, besonders Consumer Driven Contracts mit Tools wie Pact, bietet die beste Balance aus Geschwindigkeit und Zuverlässigkeit. Kombiniert mit umfassenden Request Tests und selektiven Integration Tests kannst du hohes Vertrauen erreichen dass deine Anwendungen zusammen funktionieren werden.
Und der Bonus: all diese Tests werden zu lebender Dokumentation die akkurat bleibt weil sie regelmäßig ausgeführt wird.
Die anfängliche Investition in sauberes API Testing zahlt Dividenden jedes Mal wenn du einen Produktions Incident vermeidest, jedes Mal wenn ein neues Teammitglied eine API verstehen kann indem es ihre Tests liest, jedes Mal wenn du mit Vertrauen refactorst.
Ich baue seit über einem Jahrzehnt verteilte Systeme mit API Integrationen. Die Patterns in diesem Post kommen von echten Produktionssystemen und echten Ausfällen. Die Testing Strategien sind kampferprobt.
Brauchst du Hilfe beim Etablieren von API Testing Praktiken zwischen deinen Anwendungen? Oder beim Debuggen mysteriöser Integrationsfehler? Ich kann dir helfen Systeme zu bauen die zuverlässig kommunizieren und Tests die Probleme vor Produktion fangen. Lass uns quatschen.