Integration Testing - Complete Guide

Master integration testing strategies to ensure your system components work together seamlessly

What You'll Learn

Introduction to Integration Testing

Integration testing is a level of software testing where individual units or components are combined and tested as a group. The purpose is to verify that different parts of the application work together correctly and identify issues in the interaction between integrated components.

Why Integration Testing Matters

Integration testing bridges the gap between unit tests and end-to-end tests:

  • Interface Verification: Ensures components communicate correctly through their interfaces
  • Data Flow Validation: Confirms data passes correctly between modules
  • Real Dependencies: Tests with actual databases, APIs, and external services
  • Configuration Issues: Catches problems in system configuration and setup
  • Integration Defects: Identifies bugs that only appear when components interact

Integration Testing in the Testing Pyramid

Testing Level Scope Speed Cost Quantity
Unit Tests Single component Very Fast Low Many (70-80%)
Integration Tests Multiple components Medium Medium Moderate (15-20%)
E2E Tests Entire system Slow High Few (5-10%)
Integration Sweet Spot: Integration tests provide high confidence in system behavior while remaining faster and more maintainable than end-to-end tests. They're crucial for catching real-world integration issues.

Integration Testing Approaches

Main Integration Strategies

Big Bang Integration

All or most components are combined simultaneously and tested together.

Pros: Quick to implement

Cons: Difficult to isolate defects, late detection

Best for: Small systems

Incremental Integration

Components are integrated and tested one at a time or in small groups.

Pros: Early defect detection, easier debugging

Cons: Requires more planning

Best for: Most projects

Top-Down Integration

Testing starts from top-level modules, moving downward using stubs for lower modules.

Pros: Early UI testing, top-level logic verified first

Cons: Requires stub creation, lower levels tested later

Bottom-Up Integration

Testing starts from lowest-level modules, moving upward using drivers.

Pros: Core functionality tested first, no stubs needed

Cons: UI tested late, requires test drivers

Sandwich (Hybrid)

Combination of top-down and bottom-up approaches, testing from both ends simultaneously.

Pros: Parallel testing, balanced approach

Cons: More complex, requires coordination

Risk-Based Integration

Priority given to integrating and testing high-risk or critical components first.

Pros: Early detection of critical issues

Cons: Requires thorough risk analysis

Visual Comparison

Top-Down Approach:
    [Main Module]
         |
    [Stub] [Stub]
         |
    [Module A] [Module B]
         |
    [Stub] [Real Module]

Bottom-Up Approach:
    [Driver]
         |
    [Module C] [Module D]
         |
    [Test Driver]

Sandwich Approach:
    [Top Modules] ← Test simultaneously → [Bottom Modules]
                  [Middle Layer]
                

Types of Integration Testing

Component Integration

Testing interaction between software components within the same application.

  • Class-to-class interactions
  • Module dependencies
  • Internal service calls

System Integration

Testing interaction between different systems or applications.

  • External API calls
  • Third-party services
  • Legacy system integration

API Integration

Testing REST/GraphQL/SOAP APIs and their interactions.

  • Request/response validation
  • Authentication/authorization
  • Error handling

Database Integration

Testing application interaction with databases.

  • CRUD operations
  • Transaction handling
  • Data integrity

Message Queue Integration

Testing asynchronous messaging systems.

  • Message producers/consumers
  • Event processing
  • Dead letter queues

UI Integration

Testing frontend integration with backend services.

  • API consumption
  • State management
  • Data rendering

Testing Strategies

Contract Testing

Ensures that services conform to agreed-upon contracts, especially useful in microservices architectures.

Pact Contract Testing Example

// Consumer test (Frontend)
const { Pact } = require('@pact-foundation/pact');

describe('User API', () => {
    const provider = new Pact({
        consumer: 'UserService',
        provider: 'UserAPI'
    });

    beforeAll(() => provider.setup());
    afterAll(() => provider.finalize());

    it('should get user by ID', async () => {
        // Define expected interaction
        await provider.addInteraction({
            state: 'user with ID 1 exists',
            uponReceiving: 'a request for user 1',
            withRequest: {
                method: 'GET',
                path: '/users/1',
                headers: {
                    'Accept': 'application/json'
                }
            },
            willRespondWith: {
                status: 200,
                headers: {
                    'Content-Type': 'application/json'
                },
                body: {
                    id: 1,
                    name: 'John Doe',
                    email: 'john@example.com'
                }
            }
        });

        // Test consumer
        const response = await userService.getUser(1);
        expect(response.name).toBe('John Doe');
    });
});
                

Test Doubles in Integration Testing

When to Use Mocks vs Real Dependencies

Use Real Dependencies for:

  • Database interactions (use test database)
  • Internal services you control
  • Critical integration points

Use Mocks/Stubs for:

  • External APIs (third-party services)
  • Slow or unreliable dependencies
  • Services with usage limits/costs
  • Components not yet implemented

API Integration Testing

REST API Testing

Using Supertest (Node.js/Express)

const request = require('supertest');
const app = require('../app');
const db = require('../db');

describe('User API Integration Tests', () => {
    beforeAll(async () => {
        await db.connect();
        await db.clear();
    });

    afterAll(async () => {
        await db.disconnect();
    });

    beforeEach(async () => {
        await db.seed();
    });

    describe('POST /api/users', () => {
        it('should create a new user', async () => {
            const userData = {
                name: 'Jane Doe',
                email: 'jane@example.com',
                password: 'securePassword123'
            };

            const response = await request(app)
                .post('/api/users')
                .send(userData)
                .expect('Content-Type', /json/)
                .expect(201);

            expect(response.body).toHaveProperty('id');
            expect(response.body.name).toBe(userData.name);
            expect(response.body.email).toBe(userData.email);
            expect(response.body).not.toHaveProperty('password');

            // Verify in database
            const user = await db.users.findById(response.body.id);
            expect(user).toBeDefined();
            expect(user.email).toBe(userData.email);
        });

        it('should return 400 for invalid email', async () => {
            const response = await request(app)
                .post('/api/users')
                .send({
                    name: 'Invalid User',
                    email: 'not-an-email',
                    password: 'password'
                })
                .expect(400);

            expect(response.body.error).toContain('email');
        });

        it('should return 409 for duplicate email', async () => {
            const userData = {
                name: 'Duplicate User',
                email: 'existing@example.com',
                password: 'password'
            };

            // Create first user
            await request(app).post('/api/users').send(userData);

            // Attempt duplicate
            const response = await request(app)
                .post('/api/users')
                .send(userData)
                .expect(409);

            expect(response.body.error).toContain('already exists');
        });
    });

    describe('GET /api/users/:id', () => {
        it('should retrieve user by ID', async () => {
            const user = await db.users.create({
                name: 'Test User',
                email: 'test@example.com'
            });

            const response = await request(app)
                .get(`/api/users/${user.id}`)
                .expect(200);

            expect(response.body.id).toBe(user.id);
            expect(response.body.name).toBe(user.name);
        });

        it('should return 404 for non-existent user', async () => {
            await request(app)
                .get('/api/users/99999')
                .expect(404);
        });
    });

    describe('PUT /api/users/:id', () => {
        it('should update user information', async () => {
            const user = await db.users.create({
                name: 'Original Name',
                email: 'original@example.com'
            });

            const updateData = {
                name: 'Updated Name',
                email: 'updated@example.com'
            };

            const response = await request(app)
                .put(`/api/users/${user.id}`)
                .send(updateData)
                .expect(200);

            expect(response.body.name).toBe(updateData.name);
            expect(response.body.email).toBe(updateData.email);

            // Verify in database
            const updatedUser = await db.users.findById(user.id);
            expect(updatedUser.name).toBe(updateData.name);
        });
    });

    describe('DELETE /api/users/:id', () => {
        it('should delete user', async () => {
            const user = await db.users.create({
                name: 'To Delete',
                email: 'delete@example.com'
            });

            await request(app)
                .delete(`/api/users/${user.id}`)
                .expect(204);

            // Verify deletion
            const deletedUser = await db.users.findById(user.id);
            expect(deletedUser).toBeNull();
        });
    });
});
                

Using REST Assured (Java)

import io.restassured.RestAssured;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserApiIntegrationTest {

    private static final String BASE_URL = "http://localhost:8080/api";
    private static int createdUserId;

    @BeforeAll
    public static void setup() {
        RestAssured.baseURI = BASE_URL;
    }

    @Test
    @Order(1)
    public void testCreateUser() {
        String requestBody = """
            {
                "name": "John Doe",
                "email": "john@example.com",
                "password": "password123"
            }
            """;

        createdUserId = given()
            .contentType("application/json")
            .body(requestBody)
        .when()
            .post("/users")
        .then()
            .statusCode(201)
            .body("name", equalTo("John Doe"))
            .body("email", equalTo("john@example.com"))
            .body("id", notNullValue())
        .extract()
            .path("id");
    }

    @Test
    @Order(2)
    public void testGetUser() {
        given()
            .pathParam("id", createdUserId)
        .when()
            .get("/users/{id}")
        .then()
            .statusCode(200)
            .body("id", equalTo(createdUserId))
            .body("name", equalTo("John Doe"));
    }

    @Test
    @Order(3)
    public void testUpdateUser() {
        String updateBody = """
            {
                "name": "John Updated",
                "email": "john.updated@example.com"
            }
            """;

        given()
            .contentType("application/json")
            .pathParam("id", createdUserId)
            .body(updateBody)
        .when()
            .put("/users/{id}")
        .then()
            .statusCode(200)
            .body("name", equalTo("John Updated"));
    }

    @Test
    @Order(4)
    public void testDeleteUser() {
        given()
            .pathParam("id", createdUserId)
        .when()
            .delete("/users/{id}")
        .then()
            .statusCode(204);

        // Verify deletion
        given()
            .pathParam("id", createdUserId)
        .when()
            .get("/users/{id}")
        .then()
            .statusCode(404);
    }
}
                

Database Integration Testing

Test Database Strategies

In-Memory Database

Fast, isolated tests using databases like H2, SQLite, or in-memory MongoDB.

Pros: Fast, no cleanup needed

Cons: May not match production database exactly

Test Database Instance

Dedicated test database instance with same engine as production.

Pros: Accurate, tests real database features

Cons: Slower, requires cleanup

Docker Containers

Spin up database containers for each test run using Testcontainers.

Pros: Isolated, matches production, clean state

Cons: Requires Docker, slower startup

Database Testing Examples

Using Testcontainers (Java)

import org.junit.jupiter.api.*;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    private UserRepository userRepository;

    @BeforeAll
    static void setupDatabase() {
        // Database migrations/schema setup
        Flyway flyway = Flyway.configure()
            .dataSource(postgres.getJdbcUrl(),
                       postgres.getUsername(),
                       postgres.getPassword())
            .load();
        flyway.migrate();
    }

    @BeforeEach
    void setup() {
        DataSource dataSource = createDataSource();
        userRepository = new UserRepository(dataSource);
    }

    @Test
    void testSaveAndFindUser() {
        // Given
        User user = new User("John Doe", "john@example.com");

        // When
        User savedUser = userRepository.save(user);

        // Then
        assertNotNull(savedUser.getId());

        User foundUser = userRepository.findById(savedUser.getId());
        assertEquals("John Doe", foundUser.getName());
        assertEquals("john@example.com", foundUser.getEmail());
    }

    @Test
    void testUpdateUser() {
        // Given
        User user = userRepository.save(new User("Jane", "jane@example.com"));

        // When
        user.setName("Jane Updated");
        userRepository.update(user);

        // Then
        User updated = userRepository.findById(user.getId());
        assertEquals("Jane Updated", updated.getName());
    }

    @Test
    void testDeleteUser() {
        // Given
        User user = userRepository.save(new User("Delete Me", "delete@example.com"));

        // When
        userRepository.delete(user.getId());

        // Then
        assertNull(userRepository.findById(user.getId()));
    }

    @Test
    void testFindByEmail() {
        // Given
        userRepository.save(new User("User1", "user1@example.com"));
        userRepository.save(new User("User2", "user2@example.com"));

        // When
        User found = userRepository.findByEmail("user1@example.com");

        // Then
        assertNotNull(found);
        assertEquals("User1", found.getName());
    }

    @Test
    void testTransactionRollback() {
        // Given
        User user = new User("Transaction Test", "trans@example.com");

        // When - This should rollback
        assertThrows(Exception.class, () -> {
            userRepository.saveWithError(user);
        });

        // Then - User should not be saved
        assertNull(userRepository.findByEmail("trans@example.com"));
    }
}
                

Using pytest with SQLAlchemy (Python)

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base, User

@pytest.fixture(scope='function')
def db_session():
    # Create in-memory SQLite database
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)

    Session = sessionmaker(bind=engine)
    session = Session()

    yield session

    session.close()

@pytest.fixture
def user_repository(db_session):
    return UserRepository(db_session)

def test_create_user(user_repository):
    # Given
    user_data = {
        'name': 'John Doe',
        'email': 'john@example.com'
    }

    # When
    user = user_repository.create(user_data)

    # Then
    assert user.id is not None
    assert user.name == 'John Doe'
    assert user.email == 'john@example.com'

def test_find_user_by_email(user_repository, db_session):
    # Given
    user1 = User(name='User 1', email='user1@example.com')
    user2 = User(name='User 2', email='user2@example.com')
    db_session.add_all([user1, user2])
    db_session.commit()

    # When
    found = user_repository.find_by_email('user1@example.com')

    # Then
    assert found is not None
    assert found.name == 'User 1'

def test_update_user(user_repository, db_session):
    # Given
    user = User(name='Original', email='original@example.com')
    db_session.add(user)
    db_session.commit()
    user_id = user.id

    # When
    user_repository.update(user_id, {'name': 'Updated'})

    # Then
    updated = user_repository.find_by_id(user_id)
    assert updated.name == 'Updated'
    assert updated.email == 'original@example.com'

def test_delete_user(user_repository, db_session):
    # Given
    user = User(name='Delete Me', email='delete@example.com')
    db_session.add(user)
    db_session.commit()
    user_id = user.id

    # When
    user_repository.delete(user_id)

    # Then
    assert user_repository.find_by_id(user_id) is None

def test_list_users_with_pagination(user_repository, db_session):
    # Given - Create 15 users
    for i in range(15):
        user = User(name=f'User {i}', email=f'user{i}@example.com')
        db_session.add(user)
    db_session.commit()

    # When
    page1 = user_repository.list(page=1, per_page=10)
    page2 = user_repository.list(page=2, per_page=10)

    # Then
    assert len(page1) == 10
    assert len(page2) == 5
                

Testing Microservices

Challenges in Microservices Testing

  • Distributed System Complexity: Multiple services, databases, and message queues
  • Service Dependencies: Services depend on other services
  • Network Reliability: Network calls can fail or timeout
  • Data Consistency: Eventual consistency across services
  • Environment Setup: Need to run multiple services for testing

Microservices Testing Strategies

Contract Testing

Test service contracts independently without running all services.

Tools: Pact, Spring Cloud Contract

Service Virtualization

Create virtual copies of dependent services.

Tools: WireMock, Mockito, Mountebank

Component Testing

Test service in isolation with mocked dependencies.

Approach: Test each microservice independently

Integration Testing

Test interactions between services.

Approach: Run multiple services together

Testing Microservice with WireMock

import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;

public class OrderServiceIntegrationTest {

    private WireMockServer wireMockServer;
    private OrderService orderService;

    @BeforeEach
    void setup() {
        // Start WireMock server to mock external services
        wireMockServer = new WireMockServer(8089);
        wireMockServer.start();
        configureFor("localhost", 8089);

        // Configure service to use mock server
        orderService = new OrderService("http://localhost:8089");
    }

    @AfterEach
    void teardown() {
        wireMockServer.stop();
    }

    @Test
    void testCreateOrderWithPaymentService() {
        // Mock payment service response
        stubFor(post(urlEqualTo("/api/payments"))
            .withRequestBody(matchingJsonPath("$.amount"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "paymentId": "pay_123",
                        "status": "success"
                    }
                    """)));

        // Mock inventory service response
        stubFor(post(urlEqualTo("/api/inventory/reserve"))
            .willReturn(aResponse()
                .withStatus(200)
                .withBody("""
                    {
                        "reservationId": "res_456",
                        "status": "reserved"
                    }
                    """)));

        // When
        Order order = new Order("item123", 2, 99.99);
        OrderResult result = orderService.createOrder(order);

        // Then
        assertTrue(result.isSuccess());
        assertEquals("pay_123", result.getPaymentId());

        // Verify calls were made
        verify(postRequestedFor(urlEqualTo("/api/payments"))
            .withRequestBody(matchingJsonPath("$.amount", equalTo("199.98"))));
        verify(postRequestedFor(urlEqualTo("/api/inventory/reserve"))
            .withRequestBody(matchingJsonPath("$.itemId", equalTo("item123"))));
    }

    @Test
    void testCreateOrderHandlesPaymentFailure() {
        // Mock payment service failure
        stubFor(post(urlEqualTo("/api/payments"))
            .willReturn(aResponse()
                .withStatus(402)
                .withBody("""
                    {
                        "error": "Insufficient funds"
                    }
                    """)));

        // When
        Order order = new Order("item123", 2, 99.99);
        OrderResult result = orderService.createOrder(order);

        // Then
        assertFalse(result.isSuccess());
        assertTrue(result.getError().contains("payment failed"));

        // Verify inventory was not called
        verify(0, postRequestedFor(urlEqualTo("/api/inventory/reserve")));
    }
}
                

Test Data Management

Strategies for Test Data

Test Fixtures

Pre-defined test data loaded before tests run.

Use for: Reference data, common scenarios

Test Builders

Fluent APIs to create test data programmatically.

Use for: Flexible, maintainable test data

Database Seeding

Populate database with initial data using migration tools.

Use for: Complex data relationships

Test Data Generation

Generate realistic fake data using libraries.

Tools: Faker, Chance.js, JavaFaker

Test Data Builder Pattern

// Test Data Builder
class UserTestBuilder {
    private User user;

    constructor() {
        this.user = {
            name: 'Test User',
            email: 'test@example.com',
            role: 'user',
            active: true
        };
    }

    withName(name) {
        this.user.name = name;
        return this;
    }

    withEmail(email) {
        this.user.email = email;
        return this;
    }

    asAdmin() {
        this.user.role = 'admin';
        return this;
    }

    inactive() {
        this.user.active = false;
        return this;
    }

    build() {
        return { ...this.user };
    }

    async buildAndSave() {
        const user = this.build();
        return await userRepository.create(user);
    }
}

// Usage in tests
describe('User Service Integration Tests', () => {
    it('should process admin user', async () => {
        const admin = await new UserTestBuilder()
            .withName('Admin User')
            .withEmail('admin@example.com')
            .asAdmin()
            .buildAndSave();

        const result = await userService.process(admin.id);
        expect(result.adminActionsAllowed).toBe(true);
    });

    it('should reject inactive users', async () => {
        const inactiveUser = await new UserTestBuilder()
            .withEmail('inactive@example.com')
            .inactive()
            .buildAndSave();

        await expect(userService.process(inactiveUser.id))
            .rejects.toThrow('User is inactive');
    });
});
                

Using Faker for Test Data

const { faker } = require('@faker-js/faker');

describe('User Registration', () => {
    it('should handle multiple user registrations', async () => {
        // Generate realistic test data
        const users = Array.from({ length: 10 }, () => ({
            name: faker.person.fullName(),
            email: faker.internet.email(),
            password: faker.internet.password(),
            address: {
                street: faker.location.streetAddress(),
                city: faker.location.city(),
                zipCode: faker.location.zipCode()
            },
            phone: faker.phone.number()
        }));

        // Register all users
        for (const userData of users) {
            const response = await request(app)
                .post('/api/users')
                .send(userData)
                .expect(201);

            expect(response.body.email).toBe(userData.email);
        }

        // Verify all were created
        const allUsers = await userRepository.findAll();
        expect(allUsers.length).toBeGreaterThanOrEqual(10);
    });
});
                

Tools and Frameworks

API Testing Tools

  • Postman: API development and testing platform
  • REST Assured: Java library for REST API testing
  • Supertest: Node.js HTTP assertion library
  • HttpClient: .NET testing library
  • requests: Python HTTP library

Database Testing

  • Testcontainers: Docker containers for tests
  • DbUnit: Database testing framework
  • Flyway: Database migration tool
  • Liquibase: Database schema management

Mocking & Virtualization

  • WireMock: HTTP API mocking
  • Mountebank: Service virtualization
  • MockServer: Mock HTTP/HTTPS services
  • Pact: Contract testing

Test Data Tools

  • Faker.js: Generate fake data (JavaScript)
  • JavaFaker: Generate fake data (Java)
  • Factory Boy: Test fixtures (Python)
  • Bogus: Fake data generator (.NET)

Best Practices

Test Isolation

Each test should be independent and not affect others. Clean up test data after each test.

Use Test Databases

Never run integration tests against production databases. Use dedicated test databases or containers.

Realistic Test Data

Use data that resembles production scenarios. Test edge cases and boundary conditions.

Fast Feedback

Optimize test execution time. Run critical tests first, parallelize when possible.

Clear Error Messages

When tests fail, provide detailed error messages that help identify the problem quickly.

Mock External Services

Don't depend on external third-party APIs in tests. Use mocks or stubs instead.

Test Both Happy and Sad Paths

Test successful scenarios and error conditions, timeouts, and failure cases.

Version Control Test Data

Keep test fixtures and seeds in version control for consistency across environments.

Document Test Setup

Document required services, configurations, and environment variables needed to run tests.

Common Challenges and Solutions

Challenge 1: Flaky Tests

Problem: Tests pass sometimes but fail randomly.

Solutions:

  • Avoid time-dependent tests or use controlled time
  • Ensure proper cleanup between tests
  • Add appropriate wait/retry logic for async operations
  • Use deterministic test data

Challenge 2: Slow Test Execution

Problem: Integration tests take too long to run.

Solutions:

  • Run tests in parallel when possible
  • Use in-memory databases for faster tests
  • Optimize database queries and indexes
  • Mock slow external dependencies
  • Implement test categorization (smoke, regression, etc.)

Challenge 3: Test Data Management

Problem: Managing complex test data and state.

Solutions:

  • Use test data builders for flexibility
  • Implement database transactions with rollback
  • Use data generation libraries for realistic data
  • Keep test data minimal but representative

Challenge 4: Environment Setup Complexity

Problem: Tests require complex infrastructure setup.

Solutions:

  • Use Docker Compose for multi-service setup
  • Leverage Testcontainers for automatic provisioning
  • Document setup procedures clearly
  • Create setup scripts for automation

Practical Examples

Example: E-commerce Order Processing

describe('Order Processing Integration Tests', () => {
    let db, orderService, paymentGateway, inventoryService;

    beforeAll(async () => {
        // Setup test database
        db = await setupTestDatabase();

        // Initialize services with test configuration
        orderService = new OrderService(db);
        paymentGateway = new MockPaymentGateway();
        inventoryService = new InventoryService(db);
    });

    afterAll(async () => {
        await db.close();
    });

    beforeEach(async () => {
        // Clean and seed database before each test
        await db.clean();
        await db.seed({
            products: [
                { id: 1, name: 'Widget', price: 29.99, stock: 100 },
                { id: 2, name: 'Gadget', price: 49.99, stock: 50 }
            ],
            users: [
                { id: 1, name: 'John Doe', email: 'john@example.com' }
            ]
        });
    });

    it('should successfully process order with payment and inventory', async () => {
        // Given
        const orderData = {
            userId: 1,
            items: [
                { productId: 1, quantity: 2 },
                { productId: 2, quantity: 1 }
            ]
        };

        // When
        const order = await orderService.createOrder(orderData);

        // Then
        expect(order.id).toBeDefined();
        expect(order.status).toBe('confirmed');
        expect(order.total).toBe(109.97); // (29.99 * 2) + 49.99

        // Verify payment was processed
        const payment = await db.payments.findByOrderId(order.id);
        expect(payment.status).toBe('completed');
        expect(payment.amount).toBe(109.97);

        // Verify inventory was updated
        const widget = await db.products.findById(1);
        const gadget = await db.products.findById(2);
        expect(widget.stock).toBe(98); // 100 - 2
        expect(gadget.stock).toBe(49); // 50 - 1

        // Verify order items were saved
        const orderItems = await db.orderItems.findByOrderId(order.id);
        expect(orderItems).toHaveLength(2);
    });

    it('should rollback order if payment fails', async () => {
        // Given
        paymentGateway.setShouldFail(true);
        const orderData = {
            userId: 1,
            items: [{ productId: 1, quantity: 2 }]
        };

        // When
        await expect(orderService.createOrder(orderData))
            .rejects.toThrow('Payment failed');

        // Then - Inventory should not be updated
        const widget = await db.products.findById(1);
        expect(widget.stock).toBe(100); // Original stock

        // Order should not exist or be marked as failed
        const orders = await db.orders.findByUserId(1);
        expect(orders.filter(o => o.status === 'confirmed')).toHaveLength(0);
    });

    it('should handle insufficient inventory', async () => {
        // Given - Order more than available
        const orderData = {
            userId: 1,
            items: [{ productId: 1, quantity: 150 }] // Only 100 in stock
        };

        // When
        await expect(orderService.createOrder(orderData))
            .rejects.toThrow('Insufficient inventory');

        // Then - No payment should be processed
        const payments = await db.payments.findByUserId(1);
        expect(payments).toHaveLength(0);
    });

    it('should calculate correct totals with discounts', async () => {
        // Given - Add discount code to database
        await db.discounts.create({
            code: 'SAVE20',
            percentage: 20,
            active: true
        });

        const orderData = {
            userId: 1,
            items: [{ productId: 1, quantity: 1 }],
            discountCode: 'SAVE20'
        };

        // When
        const order = await orderService.createOrder(orderData);

        // Then
        expect(order.subtotal).toBe(29.99);
        expect(order.discount).toBe(6.00);  // 20% of 29.99
        expect(order.total).toBe(23.99);    // 29.99 - 6.00

        const payment = await db.payments.findByOrderId(order.id);
        expect(payment.amount).toBe(23.99);
    });
});
                

Resources and Further Learning

Books

  • "Growing Object-Oriented Software, Guided by Tests" by Steve Freeman
  • "Continuous Delivery" by Jez Humble
  • "Release It!" by Michael Nygard
  • "Testing Microservices with Mountebank" by Brandon Byars

Online Resources

  • Martin Fowler's Integration Testing Articles
  • Testcontainers Documentation
  • REST Assured User Guide
  • Pact Documentation

Tools to Explore

  • Testcontainers for Docker-based testing
  • WireMock for API mocking
  • Pact for contract testing
  • Postman for API testing
  • k6 for performance testing
Previous: Unit Testing Next: Test Automation