API-Integrationstesting: Strategien, Tools und Best Practices
Einleitung
Unit-Tests prüfen einzelne Funktionen. API-Tests prüfen einzelne Endpunkte. Aber keiner sagt Ihnen, ob Ihre Services tatsächlich zusammenarbeiten. Das ist die Aufgabe des Integrationstestings.
API-Integrationstesting validiert, dass mehrere Services, Datenbanken und externe Systeme korrekt über ihre API-Schnittstellen kommunizieren. In einer Microservices-Architektur, in der Dutzende von Services voneinander abhängig sind, ist Integrationstesting das Sicherheitsnetz, das Vertragsabweichungen, Netzwerkfehler und Datenserialisierungsprobleme erkennt.
Dieser Leitfaden behandelt die Strategien, Tools, Code-Beispiele und Best Practices, die Sie benötigen, um zuverlässige API-Integrationstests zu erstellen, von einfachen Zwei-Service-Szenarien bis hin zu komplexen Multi-Service-Workflows.
Was ist API-Integrationstesting?
API-Integrationstesting prüft, ob verbundene Systeme korrekt über ihre API-Schnittstellen zusammenarbeiten. Im Gegensatz zu Unit-Tests (die Code isoliert testen) oder End-to-End-Tests (die das gesamte System über die UI testen) konzentriert sich Integrationstesting auf die Grenzen zwischen Services.
Was Integrationstests validieren
- Daten fließen korrekt zwischen Services (Service A sendet Daten, die Service B parsen kann)
- Verträge werden eingehalten (die API gibt die Felder und Typen zurück, die der Consumer erwartet)
- Fehlerbehandlung funktioniert über Service-Grenzen hinweg (Service A behandelt die Fehler von Service B korrekt)
- Authentifizierung wird korrekt weitergeleitet durch die Service-Kette
- Datenbankinteraktionen funktionieren korrekt (Abfragen, Transaktionen, Migrationen)
- Externe APIs verhalten sich wie erwartet (Zahlungsgateways, E-Mail-Dienste, Drittanbieter-Daten)
Integrationstesting vs. andere Testtypen
| Testtyp | Umfang | Geschwindigkeit | Abhängigkeiten | Erkennt |
|---|---|---|---|---|
| Unit-Testing | Einzelne Funktion/Klasse | Schnell (ms) | Gemockt | Logikfehler |
| API-Testing | Einzelner Endpunkt | Schnell (ms-s) | Oft gemockt | API-Vertragsverletzungen |
| Integrationstesting | Mehrere Services | Mittel (s) | Real oder Container | Schnittstellenabweichungen |
| E2E-Testing | Vollständiges System + UI | Langsam (min) | Alle real | Nutzer-Workflow-Fehler |
API-Integrationstesting-Strategien
Strategie 1: Big-Bang-Integrationstesting
Alle Services auf einmal verbinden und das vollständige System testen. Einfach zu verstehen, aber schwer zu debuggen, wenn Tests fehlschlagen, da man nicht weiß, welcher Service den Fehler verursacht hat.
Geeignet für: Kleine Systeme mit wenigen Services.
Strategie 2: Inkrementelles Integrationstesting
Services nacheinander hinzufügen und testen. Mit dem Kern-Service beginnen, dann verbundene Services schrittweise hinzufügen.
Top-down: Mit dem API-Gateway beginnen und nachgelagerte Services mocken, dann Mocks nacheinander durch echte Services ersetzen.
Bottom-up: Mit den untersten Services (Datenbank, Cache) beginnen und nach oben aufbauen.
Sandwich: Top-down und Bottom-up kombinieren und in der Mitte zusammentreffen.
Geeignet für: Mittlere bis große Systeme, bei denen Sie Fehler isolieren müssen.
Strategie 3: Contract-Testing
Verträge zwischen Consumer und Provider definieren, dann jede Seite unabhängig verifizieren. Dies ist der skalierbarste Ansatz für Microservices.
Geeignet für: Microservices-Architekturen mit vielen Inter-Service-Abhängigkeiten.
Praktisches API-Integrationstesting mit Code
JavaScript: Service-Integration mit Supertest testen
// tests/integration/orders.test.js const request = require('supertest'); const app = require('../../src/app'); const db = require('../../src/db');describe('Orders API Integration', () => { let userId; let productId;
beforeAll(async () => { // Seed database with test data await db.migrate.latest(); const user = await db('users').insert({ name: 'Test User', email: 'test@example.com' }).returning('id'); userId = user[0].id;
const product = await db('products').insert({ name: 'Widget', price: 29.99, stock: 100 }).returning('id'); productId = product[0].id;});
afterAll(async () => { await db('orders').del(); await db('products').del(); await db('users').del(); await db.destroy(); });
test('Creating an order updates product stock', async () => { // Create order via API const orderRes = await request(app) .post('/api/orders') .send({ userId, items: [{ productId, quantity: 3 }] }) .expect(201);
expect(orderRes.body.total).toBe(89.97); // 29.99 * 3 // Verify stock was decremented const productRes = await request(app) .get(`/api/products/${productId}`) .expect(200); expect(productRes.body.stock).toBe(97); // 100 - 3});
test('Order fails when insufficient stock', async () => { const res = await request(app) .post('/api/orders') .send({ userId, items: [{ productId, quantity: 9999 }] }) .expect(400);
expect(res.body.error).toContain('Insufficient stock');});
test('Order creation sends notification to user service', async () => { const orderRes = await request(app) .post('/api/orders') .send({ userId, items: [{ productId, quantity: 1 }] }) .expect(201);
// Verify notification was created const notifRes = await request(app) .get(`/api/users/${userId}/notifications`) .expect(200); const orderNotif = notifRes.body.find( n => n.type === 'order_confirmation' ); expect(orderNotif).toBeDefined(); expect(orderNotif.orderId).toBe(orderRes.body.id);
}); });
Python: Testen mit pytest und Docker
# tests/integration/test_order_flow.py import pytest import requests import timeAPI_URL = "http://localhost:3000/api"
@pytest.fixture(scope="module") def test_user(): """Create a test user and return their data.""" response = requests.post(f"{API_URL}/users", json={ "name": "Integration Test User", "email": "integration@test.com" }) assert response.status_code == 201 yield response.json() # Cleanup requests.delete(f"{API_URL}/users/{response.json()['id']}")
@pytest.fixture(scope="module") def test_product(): """Create a test product.""" response = requests.post(f"{API_URL}/products", json={ "name": "Test Widget", "price": 19.99, "stock": 50 }) assert response.status_code == 201 yield response.json() requests.delete(f"{API_URL}/products/{response.json()['id']}")
class TestOrderIntegration: def test_complete_order_flow(self, test_user, test_product): """Test the full order lifecycle across services.""" # Step 1: Create order order_response = requests.post(f"{API_URL}/orders", json={ "userId": test_user["id"], "items": [{"productId": test_product["id"], "quantity": 2}] }) assert order_response.status_code == 201 order = order_response.json() assert order["total"] == 39.98 # 19.99 * 2
# Step 2: Verify payment was processed payment_response = requests.get( f"{API_URL}/orders/{order['id']}/payment" ) assert payment_response.status_code == 200 assert payment_response.json()["status"] == "completed" # Step 3: Verify inventory updated product_response = requests.get( f"{API_URL}/products/{test_product['id']}" ) assert product_response.status_code == 200 assert product_response.json()["stock"] == 48 # 50 - 2 def test_order_rollback_on_payment_failure(self, test_user, test_product): """Verify stock is restored when payment fails.""" initial_stock = requests.get( f"{API_URL}/products/{test_product['id']}" ).json()["stock"] # Create order with invalid payment method to trigger failure order_response = requests.post(f"{API_URL}/orders", json={ "userId": test_user["id"], "items": [{"productId": test_product["id"], "quantity": 1}], "paymentMethod": "invalid_card" }) assert order_response.status_code == 400 # Verify stock was not decremented current_stock = requests.get( f"{API_URL}/products/{test_product['id']}" ).json()["stock"] assert current_stock == initial_stock
Contract-Testing mit Pact
Contract-Testing ist der effektivste Ansatz für das Testen von API-Integrationen in Microservices. Der Consumer definiert, was er vom Provider erwartet, und beide Seiten verifizieren unabhängig voneinander.
Consumer-seitiger Test (JavaScript)
// consumer/tests/userServiceClient.pact.test.js const { PactV3 } = require('@pact-foundation/pact'); const { UserServiceClient } = require('../src/userServiceClient');const provider = new PactV3({ consumer: 'OrderService', provider: 'UserService', });
describe('UserService Client', () => { test('fetches user by ID', async () => { provider .given('a user with ID 1 exists') .uponReceiving('a request for user 1') .withRequest({ method: 'GET', path: '/api/users/1', headers: { Accept: 'application/json' }, }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: { id: 1, name: 'John Doe', email: 'john@example.com', }, });
await provider.executeTest(async (mockServer) => { const client = new UserServiceClient(mockServer.url); const user = await client.getUser(1); expect(user.id).toBe(1); expect(user.name).toBe('John Doe'); });
}); });
Provider-seitige Verifizierung
// provider/tests/pactVerification.test.js const { Verifier } = require('@pact-foundation/pact');describe('UserService Provider Verification', () => { test('validates contract with OrderService', async () => { const verifier = new Verifier({ providerBaseUrl: 'http://localhost:3001', pactUrls: ['./pacts/OrderService-UserService.json'], stateHandlers: { 'a user with ID 1 exists': async () => { // Set up the required state in the provider await db('users').insert({ id: 1, name: 'John Doe', email: 'john@example.com', }); }, }, });
await verifier.verifyProvider();
}); });
Docker für Integrationstesting verwenden
Integrationstests benötigen echte Abhängigkeiten (Datenbanken, Caches, Message-Queues). Docker Compose macht dies handhabbar:
# docker-compose.test.yml version: '3.8' services: api: build: . environment: DATABASE_URL: postgres://test:test@db:5432/testdb REDIS_URL: redis://cache:6379 depends_on: db: condition: service_healthy cache: condition: service_starteddb: image: postgres:16 environment: POSTGRES_DB: testdb POSTGRES_USER: test POSTGRES_PASSWORD: test healthcheck: test: pg_isready -U test interval: 5s retries: 5
cache: image: redis:7-alpine
test-runner: build: context: . dockerfile: Dockerfile.test environment: API_URL: http://api:3000 depends_on: - api command: npm run test:integration
# Run integration tests
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit
Integrationstesting in CI/CD
# GitHub Actions integration tests
name: Integration Tests
on:
push:
branches: [main, develop]
jobs:
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run migrate
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
- run: npm run test:integration
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
Best Practices für API-Integrationstesting
1. Echte Abhängigkeiten wo möglich verwenden
Integrationstests mit gemockten Abhängigkeiten sind keine Integrationstests, sondern Unit-Tests in Verkleidung. Verwenden Sie echte Datenbanken, echte Caches und echte Message-Queues über Docker-Container.
2. Testdaten isolieren
Jeder Test sollte seine eigenen Daten erstellen und danach bereinigen. Verwenden Sie Datenbanktransaktionen, die nach jedem Test zurückgerollt werden, oder leeren Sie Tabellen zwischen Test-Runs.
3. Fehlerszenarien testen
Testen Sie nicht nur den Happy Path. Testen Sie, was passiert, wenn Abhängigkeiten nicht verfügbar sind, Fehler zurückgeben oder unerwartete Daten liefern.
4. Tests schnell halten
Integrationstests sind langsamer als Unit-Tests, aber sie sollten nicht zu langsam sein. Ziel ist unter 30 Sekunden für Ihre vollständige Integrationssuite. Verwenden Sie parallele Ausführung und geteiltes Setup wo sinnvoll.
5. Contract-Testing für teamübergreifende APIs verwenden
Wenn verschiedene Teams verschiedene Services besitzen, ist Contract-Testing (mit Pact oder ähnlichen Tools) praktikabler als das gleichzeitige Ausführen aller Services.
6. Mit anderen Testtypen kombinieren
Integrationstests ergänzen REST-API-Tests, Load-Tests und Security-Tests. Verwenden Sie Qodex.ai, um funktionale und Security-Tests automatisch zu generieren, und fügen Sie dann Integrationstests für Cross-Service-Workflows hinzu.
Einen vollständigen Überblick über Testing-Tools finden Sie in unserem API-Testing-Tools-Vergleich.
Häufig gestellte Fragen
Was ist der Unterschied zwischen API-Testing und API-Integrationstesting?
API-Testing validiert einen einzelnen API-Endpunkt isoliert: korrekte Statuscodes, Response-Bodies und Fehlerbehandlung. API-Integrationstesting validiert, dass mehrere Services über ihre APIs zusammenarbeiten, Daten korrekt fließen, Verträge eingehalten werden und Fehler korrekt über Service-Grenzen hinweg weitergegeben werden.
Ist API-Testing dasselbe wie Integrationstesting?
Nicht genau. API-Testing kann isoliert durchgeführt werden (mit gemockten Abhängigkeiten), was es eher dem Unit-Testing ähnelt. Integrationstesting prüft speziell die Interaktionen zwischen echten Services, Datenbanken und externen Systemen. Es gibt jedoch erhebliche Überschneidungen, und viele Teams verwenden API-Tests als Integrationstests, wenn sie gegen echte Abhängigkeiten testen.
Wie teste ich API-Integration ohne Zugang zum echten Service?
Verwenden Sie Contract-Testing mit Tools wie Pact. Der Consumer definiert erwartete Interaktionen, und beide Seiten verifizieren unabhängig voneinander. Sie können auch Mock-Server, WireMock oder Service-Virtualisierungs-Tools verwenden, um den externen Service zu simulieren.
Welche Tools eignen sich am besten für API-Integrationstesting?
Für JavaScript: Supertest + Jest mit Docker. Für Python: pytest + requests mit Docker. Für Contract-Testing: Pact. Für automatisierte Testgenerierung: Qodex.ai. Für Cross-Service-Testing: Docker Compose zur Orchestrierung aller Services.
Wie gehe ich mit Testdaten in Integrationstests um?
Verwenden Sie Datenbanktransaktionen, die nach jedem Test zurückgerollt werden, oder leeren Sie Tabellen zwischen Test-Runs. Erstellen Sie Testdaten in setUp/beforeEach-Hooks und bereinigen Sie in tearDown/afterEach. Teilen Sie niemals Testdaten zwischen Tests, jeder Test sollte unabhängig sein.
Sollten Integrationstests in CI/CD laufen?
Ja. Integrationstests sollten bei jedem Push und Pull Request laufen. Verwenden Sie Docker Compose oder CI/CD-Service-Container (wie GitHub Actions Services), um echte Abhängigkeiten hochzufahren. Halten Sie die Suite schnell, indem Sie sich auf kritische Integrationspfade konzentrieren und Tests parallel ausführen.
Discover, Test, & Secure your APIs 10x Faster than before
Auto-discover every endpoint, generate functional & security tests (OWASP Top 10), auto-heal as code changes, and run in CI/CD - no code needed.
Related Blogs





