32 Integration Tests

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.

32.1 Was sind Integration Tests?

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.

32.1.1 Abgrenzung zu anderen Testarten

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
});

32.2 Arten von Integration Tests

32.2.1 Komponenten-Integration

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
  });
});

32.2.2 API-Integration Tests

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);
  });
});

32.2.3 Datenbank-Integration Tests

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();
  });
});

32.3 Integration Test Patterns

32.3.1 Test Data Builder Pattern

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');
  });
});

32.3.2 Container Testing Pattern

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;

32.4 Herausforderungen und Best Practices

32.4.1 Test-Isolation und Cleanup

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
  });
});

32.4.2 Performance-Optimierung

// 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
});

32.4.3 Fehlerbehandlung und Debugging

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;
    }
  });
});

32.5 Testing-Tools für Integration Tests

32.5.1 Supertest für HTTP-Integration Tests

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');
      });
  });
});