Integration Tests überprüfen das Zusammenspiel zwischen verschiedenen Komponenten, Modulen oder Services einer Anwendung. Sie füllen die Lücke zwischen Unit Tests, die isolierte Funktionen testen, und End-to-End Tests, die komplette Benutzerszenarien abbilden.
Integration Tests validieren, dass verschiedene Teile einer Anwendung korrekt miteinander interagieren. Sie testen die Schnittstellen zwischen Modulen, die Datenübertragung zwischen Komponenten und die korrekte Implementierung von Contracts zwischen Services.
Während Unit Tests einzelne Funktionen in Isolation testen und dabei Abhängigkeiten durch Mocks ersetzen, verwenden Integration Tests echte Implementierungen der beteiligten Komponenten. Im Gegensatz zu End-to-End Tests, die in den entsprechenden Kapiteln behandelt werden, konzentrieren sich Integration Tests auf spezifische Interaktionen ohne die komplette Benutzeroberfläche.
// Unit Test: Isoliert mit Mocks
test('user service unit test', () => {
const mockDatabase = { findUser: jest.fn().mockReturnValue(user) };
const userService = new UserService(mockDatabase);
// Test der UserService-Logik isoliert
});
// Integration Test: Echte Komponenten
test('user service integration test', async () => {
const database = new TestDatabase();
const userService = new UserService(database);
await database.seed(testData);
// Test der tatsächlichen Interaktion zwischen Service und Database
});Diese Tests überprüfen das Zusammenspiel zwischen verschiedenen Modulen oder Klassen innerhalb einer Anwendung.
// order.js
class Order {
constructor(paymentService, inventoryService, emailService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
this.emailService = emailService;
this.items = [];
this.status = 'pending';
}
async addItem(productId, quantity) {
const available = await this.inventoryService.checkAvailability(productId, quantity);
if (!available) {
throw new Error('Insufficient inventory');
}
this.items.push({ productId, quantity });
}
async process() {
const total = await this.calculateTotal();
const payment = await this.paymentService.charge(total);
if (payment.success) {
await this.inventoryService.reserve(this.items);
await this.emailService.sendConfirmation(this.customerEmail, this);
this.status = 'confirmed';
} else {
this.status = 'failed';
}
return this.status;
}
}// order.integration.test.js
const Order = require('./order');
const PaymentService = require('./paymentService');
const InventoryService = require('./inventoryService');
const EmailService = require('./emailService');
describe('Order Integration Tests', () => {
let paymentService, inventoryService, emailService;
beforeEach(() => {
// Echte Service-Instanzen, aber mit Test-Konfiguration
paymentService = new PaymentService({ testMode: true });
inventoryService = new InventoryService({ database: 'test' });
emailService = new EmailService({ provider: 'test' });
});
test('should process order successfully with all services', async () => {
const order = new Order(paymentService, inventoryService, emailService);
order.customerEmail = 'test@example.com';
// Setup: Produkt im Test-Inventar verfügbar machen
await inventoryService.addProduct('product-1', 10);
await order.addItem('product-1', 2);
const result = await order.process();
expect(result).toBe('confirmed');
// Überprüfung der Service-Interaktionen
const inventory = await inventoryService.getAvailability('product-1');
expect(inventory).toBe(8); // 10 - 2 reserviert
const payments = await paymentService.getTestTransactions();
expect(payments).toHaveLength(1);
expect(payments[0].status).toBe('success');
});
test('should handle payment failure gracefully', async () => {
// Payment Service für Fehler konfigurieren
paymentService.setTestMode('decline_all');
const order = new Order(paymentService, inventoryService, emailService);
await order.addItem('product-1', 1);
const result = await order.process();
expect(result).toBe('failed');
// Inventar sollte nicht reserviert worden sein
const inventory = await inventoryService.getAvailability('product-1');
expect(inventory).toBe(10); // Unverändert
});
});Diese Tests überprüfen die Interaktion mit externen APIs oder zwischen verschiedenen API-Endpunkten.
// apiClient.js
class ApiClient {
constructor(baseUrl, authToken) {
this.baseUrl = baseUrl;
this.authToken = authToken;
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
}
module.exports = ApiClient;// api.integration.test.js
const ApiClient = require('./apiClient');
const testServer = require('./testServer');
describe('API Integration Tests', () => {
let server, apiClient;
beforeAll(async () => {
// Test-Server starten
server = await testServer.start();
apiClient = new ApiClient('http://localhost:3001', 'test-token');
});
afterAll(async () => {
await server.close();
});
beforeEach(async () => {
// Test-Datenbank zurücksetzen
await server.resetDatabase();
await server.seedTestData();
});
test('should create and retrieve user via API', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
// User erstellen
const createdUser = await apiClient.post('/users', userData);
expect(createdUser).toHaveProperty('id');
expect(createdUser.name).toBe(userData.name);
expect(createdUser.email).toBe(userData.email);
// User abrufen
const retrievedUser = await apiClient.get(`/users/${createdUser.id}`);
expect(retrievedUser).toEqual(createdUser);
});
test('should handle authentication errors', async () => {
const unauthorizedClient = new ApiClient('http://localhost:3001', 'invalid-token');
await expect(unauthorizedClient.get('/users/1'))
.rejects
.toThrow('API Error: 401');
});
test('should validate data consistency across endpoints', async () => {
// User erstellen
const user = await apiClient.post('/users', {
name: 'Jane Smith',
email: 'jane@example.com'
});
// Profil für User erstellen
const profile = await apiClient.post('/profiles', {
userId: user.id,
bio: 'Software Developer',
location: 'Berlin'
});
// Überprüfen, dass User-Liste das neue Profil referenziert
const users = await apiClient.get('/users');
const userWithProfile = users.find(u => u.id === user.id);
expect(userWithProfile.profileId).toBe(profile.id);
});
});Diese Tests überprüfen die Interaktion zwischen Anwendungslogik und Datenbank.
// userRepository.js
class UserRepository {
constructor(database) {
this.db = database;
}
async create(userData) {
const query = 'INSERT INTO users (name, email, created_at) VALUES (?, ?, ?) RETURNING *';
const result = await this.db.query(query, [
userData.name,
userData.email,
new Date()
]);
return result.rows[0];
}
async findByEmail(email) {
const query = 'SELECT * FROM users WHERE email = ?';
const result = await this.db.query(query, [email]);
return result.rows[0] || null;
}
async updateLastLogin(userId) {
const query = 'UPDATE users SET last_login = ? WHERE id = ?';
await this.db.query(query, [new Date(), userId]);
}
async getUserStats() {
const queries = await Promise.all([
this.db.query('SELECT COUNT(*) as total FROM users'),
this.db.query('SELECT COUNT(*) as active FROM users WHERE last_login > ?', [
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days ago
])
]);
return {
total: queries[0].rows[0].total,
active: queries[1].rows[0].active
};
}
}
module.exports = UserRepository;// userRepository.integration.test.js
const UserRepository = require('./userRepository');
const Database = require('./database');
describe('UserRepository Integration Tests', () => {
let database, userRepository;
beforeAll(async () => {
database = new Database({
host: 'localhost',
database: 'test_db',
user: 'test_user',
password: 'test_password'
});
await database.connect();
userRepository = new UserRepository(database);
});
afterAll(async () => {
await database.disconnect();
});
beforeEach(async () => {
// Tabellen leeren
await database.query('DELETE FROM users');
});
test('should create and retrieve user from database', async () => {
const userData = {
name: 'Alice Johnson',
email: 'alice@example.com'
};
const createdUser = await userRepository.create(userData);
expect(createdUser.id).toBeDefined();
expect(createdUser.name).toBe(userData.name);
expect(createdUser.email).toBe(userData.email);
expect(createdUser.created_at).toBeInstanceOf(Date);
const foundUser = await userRepository.findByEmail(userData.email);
expect(foundUser).toEqual(createdUser);
});
test('should handle duplicate email constraint', async () => {
const userData = { name: 'Bob Smith', email: 'bob@example.com' };
await userRepository.create(userData);
await expect(userRepository.create(userData))
.rejects
.toThrow(/duplicate key value violates unique constraint/i);
});
test('should calculate user statistics correctly', async () => {
// Setup: Mehrere User mit verschiedenen Login-Zeiten erstellen
const users = [
{ name: 'User1', email: 'user1@example.com' },
{ name: 'User2', email: 'user2@example.com' },
{ name: 'User3', email: 'user3@example.com' }
];
for (const userData of users) {
await userRepository.create(userData);
}
// Einen User als kürzlich aktiv markieren
const activeUser = await userRepository.findByEmail('user1@example.com');
await userRepository.updateLastLogin(activeUser.id);
const stats = await userRepository.getUserStats();
expect(stats.total).toBe(3);
expect(stats.active).toBe(1);
});
test('should handle transaction rollback', async () => {
await database.beginTransaction();
try {
await userRepository.create({ name: 'Test User', email: 'test@example.com' });
// Simuliere einen Fehler
throw new Error('Simulated error');
} catch (error) {
await database.rollback();
}
// User sollte nicht in der Datenbank sein
const user = await userRepository.findByEmail('test@example.com');
expect(user).toBeNull();
});
});Das Test Data Builder Pattern hilft dabei, komplexe Testdaten strukturiert aufzubauen.
// testDataBuilder.js
class UserBuilder {
constructor() {
this.userData = {
name: 'Default User',
email: 'default@example.com',
active: true,
role: 'user'
};
}
withName(name) {
this.userData.name = name;
return this;
}
withEmail(email) {
this.userData.email = email;
return this;
}
asAdmin() {
this.userData.role = 'admin';
return this;
}
inactive() {
this.userData.active = false;
return this;
}
build() {
return { ...this.userData };
}
}
class OrderBuilder {
constructor() {
this.orderData = {
items: [],
status: 'pending',
total: 0
};
}
withItem(productId, quantity, price) {
this.orderData.items.push({ productId, quantity, price });
this.orderData.total += quantity * price;
return this;
}
withStatus(status) {
this.orderData.status = status;
return this;
}
build() {
return { ...this.orderData };
}
}
module.exports = { UserBuilder, OrderBuilder };// integration.test.js
const { UserBuilder, OrderBuilder } = require('./testDataBuilder');
describe('E-Commerce Integration Tests', () => {
test('should process order for admin user', async () => {
const adminUser = new UserBuilder()
.withName('Admin User')
.withEmail('admin@company.com')
.asAdmin()
.build();
const order = new OrderBuilder()
.withItem('laptop', 1, 999.99)
.withItem('mouse', 2, 29.99)
.build();
const result = await orderService.processOrder(adminUser, order);
expect(result.status).toBe('approved');
});
});Für komplexe Integration Tests können Test-Container verwendet werden, um isolierte Umgebungen zu schaffen.
// testContainer.js
class TestContainer {
constructor() {
this.services = new Map();
this.database = null;
}
async start() {
// Database Container starten
this.database = await this.startDatabase();
// Services mit Test-Database initialisieren
this.registerService('userRepository', new UserRepository(this.database));
this.registerService('orderService', new OrderService(
this.getService('userRepository'),
new PaymentService({ testMode: true })
));
}
async stop() {
await this.database.disconnect();
this.services.clear();
}
registerService(name, service) {
this.services.set(name, service);
}
getService(name) {
return this.services.get(name);
}
async startDatabase() {
const db = new Database({ database: 'integration_test' });
await db.connect();
await db.migrate();
return db;
}
async reset() {
await this.database.truncateAllTables();
await this.database.seedTestData();
}
}
module.exports = TestContainer;describe('Integration Tests with proper cleanup', () => {
let testContainer;
beforeAll(async () => {
testContainer = new TestContainer();
await testContainer.start();
});
afterAll(async () => {
await testContainer.stop();
});
beforeEach(async () => {
// Jeden Test mit sauberer Umgebung starten
await testContainer.reset();
});
test('test with guaranteed clean state', async () => {
// Test-Logik hier
});
});// Parallele Test-Ausführung mit isolierten Datenbanken
describe('Parallel Integration Tests', () => {
const testId = Math.random().toString(36).substring(7);
let database;
beforeAll(async () => {
// Eindeutige Test-Database pro Test-Suite
database = new Database({
database: `test_${testId}`,
isolation: 'suite'
});
await database.connect();
});
// Tests können parallel in verschiedenen Suites laufen
});describe('Integration Tests with debugging support', () => {
test('detailed error information on failure', async () => {
try {
const result = await complexIntegrationOperation();
expect(result.status).toBe('success');
} catch (error) {
// Detaillierte Debugging-Informationen sammeln
const debugInfo = {
error: error.message,
stack: error.stack,
databaseState: await database.getCurrentState(),
serviceStates: await gatherServiceStates()
};
console.error('Integration test failed:', JSON.stringify(debugInfo, null, 2));
throw error;
}
});
});const request = require('supertest');
const app = require('./app');
describe('HTTP Integration Tests', () => {
test('should handle complete user workflow', async () => {
// User registrieren
const registerResponse = await request(app)
.post('/api/register')
.send({
name: 'Integration User',
email: 'integration@example.com',
password: 'securepassword'
})
.expect(201);
const userId = registerResponse.body.id;
// User einloggen
const loginResponse = await request(app)
.post('/api/login')
.send({
email: 'integration@example.com',
password: 'securepassword'
})
.expect(200);
const token = loginResponse.body.token;
// Authentifizierte Anfrage
await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200)
.expect(res => {
expect(res.body.name).toBe('Integration User');
});
});
});