Skip to content

Integration Testing

Alex Stojcic edited this page Apr 7, 2025 · 1 revision

Integration Testing

Integration testing is the phase in software testing where individual software modules are combined and tested as a group. The purpose is to verify that different components of the application work together correctly, exposing defects in the interfaces and interactions between integrated components.

Purpose of Integration Testing

  • Verify that components work together as expected
  • Test the communication between different modules
  • Identify issues in the interaction between integrated components
  • Ensure that interfaces between components are functioning correctly
  • Validate the workflow across multiple components

Types of Integration Testing

1. Big Bang Integration Testing

All components are integrated simultaneously and tested as a complete system.

Advantages:

  • Simple approach for small systems
  • No need for stubs or drivers

Disadvantages:

  • Difficult to localize faults
  • Integration issues found late in the process
  • Not suitable for large systems

2. Incremental Integration Testing

Components are integrated and tested one at a time.

2.1 Top-Down Integration

Start with top-level modules and gradually add lower-level modules.

Advantages:

  • Early testing of high-level logic
  • Requires only stubs for lower-level components
  • Major design issues detected early

Disadvantages:

  • Basic functionality tested late
  • Requires many stubs
  • Lower-level modules not tested independently

2.2 Bottom-Up Integration

Start with lower-level modules and gradually move up.

Advantages:

  • Basic functionality tested early
  • No stubs required, only test drivers
  • Good when lower-level modules are critical or risky

Disadvantages:

  • High-level logic tested late
  • Requires test drivers
  • UI tested late in the process

2.3 Sandwich/Hybrid Integration

Combine top-down and bottom-up approaches.

Advantages:

  • Leverages advantages of both approaches
  • Suitable for large, complex systems
  • Tests critical middle layers early

Disadvantages:

  • More complex to plan and execute
  • Requires both stubs and drivers
  • May need additional resources

Integration Test Scope

API Integration Testing

Tests APIs and the interactions between different services. Focuses on:

  • Request/response validation
  • Error handling
  • Authentication and authorization
  • API performance and scalability

Database Integration Testing

Tests the interaction between an application and its database. Focuses on:

  • CRUD operations
  • Transaction management
  • Data integrity constraints
  • Migration scripts

Service Integration Testing

Tests the interaction between microservices or service layers. Focuses on:

  • Service contracts
  • Message formats
  • Fault tolerance
  • Asynchronous communication

Third-Party Integration Testing

Tests the integration with external services and libraries. Focuses on:

  • Authentication with external systems
  • Data mapping between systems
  • Error handling for external service failures
  • API versioning and compatibility

Best Practices for Integration Testing

1. Use Test Databases

Create a dedicated test database environment:

// Example of setting up a test database in Jest
beforeAll(async () => {
  // Connect to test database
  testDb = await connectToTestDatabase();
  
  // Seed with test data
  await seedTestData(testDb);
});

afterAll(async () => {
  // Clean up test database
  await cleanupTestDatabase(testDb);
  await disconnectFromTestDatabase(testDb);
});

2. Isolate Tests with Transactions

Use database transactions to ensure test isolation:

// Example using a transaction in a test
beforeEach(async () => {
  // Start a transaction
  transaction = await db.beginTransaction();
});

afterEach(async () => {
  // Roll back the transaction after each test
  await transaction.rollback();
});

test('should create a user and associated profile', async () => {
  // Test code that creates a user and profile
  await userService.createUserWithProfile(userData, profileData);
  
  // Verify user and profile were created
  const user = await transaction.findUserByEmail(userData.email);
  const profile = await transaction.findProfileByUserId(user.id);
  
  expect(user).not.toBeNull();
  expect(profile).not.toBeNull();
  expect(profile.userId).toBe(user.id);
});

3. Mock External Dependencies When Appropriate

Use mocks for external services that are not the focus of the integration test:

// Example mocking a payment service while testing order processing
test('should process order and apply payment', async () => {
  // Mock the payment service
  const mockPaymentService = {
    processPayment: jest.fn().mockResolvedValue({
      success: true,
      transactionId: 'mock-transaction-123'
    })
  };
  
  // Inject the mock into the order service
  const orderService = new OrderService({
    paymentService: mockPaymentService,
    // Real database connection for integration testing
    orderRepository: orderRepository
  });
  
  // Test order processing
  const result = await orderService.createOrder(orderData);
  
  // Verify order was saved to the database
  const savedOrder = await orderRepository.findById(result.orderId);
  expect(savedOrder).not.toBeNull();
  expect(savedOrder.status).toBe('paid');
  
  // Verify payment service was called correctly
  expect(mockPaymentService.processPayment).toHaveBeenCalledWith({
    amount: orderData.totalAmount,
    paymentMethod: orderData.paymentMethod
  });
});

4. Set Up Realistic Test Environments

Create test environments that closely match production:

  • Use Docker containers to simulate production services
  • Set up test instances of databases and message queues
  • Configure test services with realistic settings

5. Test Happy Paths and Edge Cases

Cover both standard and exceptional flows:

// Happy path test
test('should successfully transfer money between accounts', async () => {
  // Test successful money transfer
});

// Edge case tests
test('should handle insufficient funds during transfer', async () => {
  // Test transfer with insufficient balance
});

test('should handle transfer to closed account', async () => {
  // Test transfer to an account that is closed
});

test('should handle concurrent transfers to the same account', async () => {
  // Test race conditions with multiple simultaneous transfers
});

6. Use Contract Testing for Service Integration

Implement contract tests to verify service interfaces:

// Consumer-driven contract test example
test('user service returns data in the expected format', async () => {
  // Call the user service
  const response = await userService.getUserProfile(userId);
  
  // Verify the contract is fulfilled
  expect(response).toMatchSchema({
    type: 'object',
    required: ['id', 'name', 'email', 'createdAt'],
    properties: {
      id: { type: 'string' },
      name: { type: 'string' },
      email: { type: 'string', format: 'email' },
      createdAt: { type: 'string', format: 'date-time' }
    }
  });
});

7. Set Up Consistent Test Data

Create helper functions for test data setup:

// Example test data setup function
async function setupOrderWithProducts() {
  const customer = await createTestCustomer();
  const products = await createTestProducts(3);
  const order = await createTestOrder(customer.id, products);
  
  return { customer, products, order };
}

test('should calculate order totals correctly', async () => {
  // Set up test data
  const { order, products } = await setupOrderWithProducts();
  
  // Test calculation logic
  await orderService.calculateTotals(order.id);
  
  // Verify results
  const updatedOrder = await orderRepository.findById(order.id);
  const expectedTotal = products.reduce((sum, p) => sum + p.price, 0);
  
  expect(updatedOrder.subtotal).toBeCloseTo(expectedTotal);
  expect(updatedOrder.tax).toBeCloseTo(expectedTotal * 0.1); // Assuming 10% tax
  expect(updatedOrder.total).toBeCloseTo(expectedTotal * 1.1);
});

Popular Integration Testing Tools

API Testing Tools

  • Postman/Newman: HTTP API testing with collection runners
  • REST Assured: Java library for REST API testing
  • Supertest: Node.js HTTP assertion library
  • Pact: Contract testing for APIs

Database Testing Tools

  • Testcontainers: Provides lightweight, throwaway instances of databases
  • Flyway/Liquibase: Database migration tools useful for test setup
  • DBUnit: Database testing framework for Java
  • pg-mem: In-memory PostgreSQL for Node.js testing

Service Testing Tools

  • WireMock: Mock HTTP services for testing
  • MockServer: Easy mocking of HTTP dependencies
  • Hoverfly: Lightweight service virtualization
  • Docker Compose: For setting up multi-service test environments

Example Integration Test Scenarios

1. User Registration Flow

test('complete user registration flow', async () => {
  // Test user data
  const userData = {
    email: 'test@example.com',
    password: 'securePassword123',
    name: 'Test User'
  };
  
  // 1. Register the user
  const registrationResult = await userService.register(userData);
  expect(registrationResult.success).toBe(true);
  
  // 2. Verify user is saved in the database
  const savedUser = await userRepository.findByEmail(userData.email);
  expect(savedUser).not.toBeNull();
  expect(savedUser.name).toBe(userData.name);
  expect(savedUser.isActive).toBe(false); // Should be inactive until email verification
  
  // 3. Verify verification email was sent
  expect(emailService.sendVerification).toHaveBeenCalledWith(
    userData.email,
    expect.any(String) // Verification token
  );
  
  // 4. Verify user account with the token
  const verificationToken = emailService.sendVerification.mock.calls[0][1];
  const verificationResult = await userService.verifyEmail(verificationToken);
  expect(verificationResult.success).toBe(true);
  
  // 5. Verify user is now active
  const activeUser = await userRepository.findByEmail(userData.email);
  expect(activeUser.isActive).toBe(true);
  
  // 6. Verify user can log in
  const loginResult = await authService.login(userData.email, userData.password);
  expect(loginResult.success).toBe(true);
  expect(loginResult.token).toBeDefined();
});

2. Order Processing Flow

test('end-to-end order processing flow', async () => {
  // 1. Create test customer and products
  const customer = await createTestCustomer();
  const products = await createTestProducts(2);
  
  // 2. Create a shopping cart
  const cart = await cartService.createCart(customer.id);
  
  // 3. Add products to cart
  await cartService.addItem(cart.id, products[0].id, 2); // 2 quantity
  await cartService.addItem(cart.id, products[1].id, 1); // 1 quantity
  
  // 4. Apply a coupon
  const coupon = await createTestCoupon('TESTCODE', 10); // 10% discount
  await cartService.applyCoupon(cart.id, coupon.code);
  
  // 5. Create order from cart
  const order = await orderService.createFromCart(cart.id);
  
  // 6. Verify order details
  expect(order.customerId).toBe(customer.id);
  expect(order.items.length).toBe(2);
  expect(order.items[0].productId).toBe(products[0].id);
  expect(order.items[0].quantity).toBe(2);
  expect(order.discountPercentage).toBe(10);
  
  // 7. Process payment
  const paymentResult = await paymentService.processOrderPayment(order.id, {
    type: 'credit_card',
    cardNumber: '4111111111111111', // Test card number
    expiryMonth: '12',
    expiryYear: '2030',
    cvv: '123'
  });
  
  expect(paymentResult.success).toBe(true);
  
  // 8. Verify order status is updated
  const updatedOrder = await orderRepository.findById(order.id);
  expect(updatedOrder.status).toBe('paid');
  expect(updatedOrder.paymentId).toBe(paymentResult.transactionId);
  
  // 9. Check inventory was updated
  const updatedProduct1 = await productRepository.findById(products[0].id);
  const updatedProduct2 = await productRepository.findById(products[1].id);
  
  expect(updatedProduct1.stockQuantity).toBe(products[0].stockQuantity - 2);
  expect(updatedProduct2.stockQuantity).toBe(products[1].stockQuantity - 1);
});

Examples from the Repository

Our repository includes several integration testing examples:

Related Wiki Pages