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 time
API_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_started
db:
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.





