Netspective Logo
Automated Testing

Integration Testing

Testing interactions between components, services, and external systems

Integration tests verify that different parts of your system work together correctly. They sit in the middle of the testing pyramid, providing confidence that components integrate properly without the overhead of full end-to-end tests.

What is Integration Testing?

Integration tests verify the interaction between:

  • Multiple classes/modules within your application
  • Your application and external services (databases, APIs)
  • Different layers of your architecture (API → Service → Repository)

Integration vs Unit Tests

AspectUnit TestIntegration Test
ScopeSingle function/classMultiple components
DependenciesMockedReal or controlled
SpeedVery fast (ms)Slower (seconds)
DatabaseNeverOften included
NetworkNeverSometimes included
IsolationCompletePartial

Integration Testing Strategies

Big Bang Integration

Test all components together at once:

  • Pros: Simple to set up
  • Cons: Difficult to isolate failures

Incremental Integration

Test components progressively:

Integration Testing Strategies

Top-Down: Start from high-level modules, stub lower levels

Bottom-Up: Start from low-level modules

Sandwich/Hybrid: Combine both approaches


API Integration Testing

Testing REST Endpoints

import request from 'supertest';
import { app } from '../src/app';
import { prisma } from '../src/db';

describe('POST /api/users', () => {
  beforeEach(async () => {
    await prisma.user.deleteMany();
  });

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

  it('should create a new user', async () => {
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'SecurePass123!',
    };

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

    expect(response.body).toMatchObject({
      id: expect.any(Number),
      name: 'John Doe',
      email: 'john@example.com',
    });
    expect(response.body).not.toHaveProperty('password');

    // Verify database state
    const dbUser = await prisma.user.findUnique({
      where: { email: 'john@example.com' },
    });
    expect(dbUser).toBeDefined();
    expect(dbUser?.name).toBe('John Doe');
  });

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

    expect(response.body.errors).toContainEqual(
      expect.objectContaining({ field: 'email' })
    );
  });

  it('should return 409 for duplicate email', async () => {
    await prisma.user.create({
      data: { name: 'Existing', email: 'john@example.com', passwordHash: 'xxx' },
    });

    await request(app)
      .post('/api/users')
      .send({ name: 'John', email: 'john@example.com', password: 'Pass123!' })
      .expect(409);
  });
});

Testing Authentication

describe('Authentication Integration', () => {
  let authToken: string;

  beforeAll(async () => {
    // Create test user
    await request(app)
      .post('/api/users')
      .send({ name: 'Test', email: 'test@example.com', password: 'Pass123!' });
  });

  it('should login with valid credentials', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ email: 'test@example.com', password: 'Pass123!' })
      .expect(200);

    expect(response.body.token).toBeDefined();
    authToken = response.body.token;
  });

  it('should access protected route with token', async () => {
    await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);
  });

  it('should reject access without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });
});

Database Integration Testing

Using Test Databases

// test-setup.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({
  datasources: {
    db: { url: process.env.TEST_DATABASE_URL },
  },
});

beforeAll(async () => {
  // Run migrations on test database
  await prisma.$executeRaw`TRUNCATE TABLE users CASCADE`;
});

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

export { prisma };

Transaction Rollback Pattern

Keep test database clean by rolling back transactions:

describe('OrderService', () => {
  let tx: PrismaClient;

  beforeEach(async () => {
    // Start transaction
    tx = await prisma.$transaction(async (prisma) => {
      return prisma;
    });
  });

  afterEach(async () => {
    // Rollback by throwing
    await tx.$disconnect();
  });

  it('should create order with items', async () => {
    const order = await orderService.createOrder(tx, {
      customerId: 1,
      items: [{ productId: 1, quantity: 2 }],
    });

    expect(order.items).toHaveLength(1);
    expect(order.total).toBeGreaterThan(0);
  });
});

Testing Repository Layer

describe('UserRepository', () => {
  const repository = new UserRepository(prisma);

  beforeEach(async () => {
    await prisma.user.deleteMany();
  });

  describe('findByEmail', () => {
    it('should find existing user', async () => {
      await prisma.user.create({
        data: {
          email: 'test@example.com',
          name: 'Test User',
          passwordHash: 'hashed',
        },
      });

      const user = await repository.findByEmail('test@example.com');

      expect(user).not.toBeNull();
      expect(user?.name).toBe('Test User');
    });

    it('should return null for non-existent user', async () => {
      const user = await repository.findByEmail('nonexistent@example.com');
      expect(user).toBeNull();
    });

    it('should be case-insensitive', async () => {
      await prisma.user.create({
        data: {
          email: 'Test@Example.com',
          name: 'Test',
          passwordHash: 'hashed',
        },
      });

      const user = await repository.findByEmail('test@example.com');
      expect(user).not.toBeNull();
    });
  });
});

External Service Integration

Mocking External APIs

Use tools like MSW (Mock Service Worker) for API mocking:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('https://api.stripe.com/v1/customers/:id', (req, res, ctx) => {
    return res(
      ctx.json({
        id: req.params.id,
        email: 'customer@example.com',
        name: 'Test Customer',
      })
    );
  }),

  rest.post('https://api.stripe.com/v1/charges', (req, res, ctx) => {
    return res(
      ctx.json({
        id: 'ch_test_123',
        status: 'succeeded',
        amount: 1000,
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('PaymentService', () => {
  it('should process payment successfully', async () => {
    const result = await paymentService.charge({
      customerId: 'cus_123',
      amount: 1000,
    });

    expect(result.status).toBe('succeeded');
  });

  it('should handle payment failure', async () => {
    server.use(
      rest.post('https://api.stripe.com/v1/charges', (req, res, ctx) => {
        return res(
          ctx.status(402),
          ctx.json({ error: { message: 'Card declined' } })
        );
      })
    );

    await expect(paymentService.charge({
      customerId: 'cus_123',
      amount: 1000,
    })).rejects.toThrow('Card declined');
  });
});

Contract Testing

Verify your API matches expected contracts:

import { Pact } from '@pact-foundation/pact';

const provider = new Pact({
  consumer: 'OrderService',
  provider: 'InventoryService',
});

describe('Inventory API Contract', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());
  afterEach(() => provider.verify());

  it('should check product availability', async () => {
    await provider.addInteraction({
      state: 'product 123 has 10 items in stock',
      uponReceiving: 'a request for product availability',
      withRequest: {
        method: 'GET',
        path: '/api/inventory/123',
      },
      willRespondWith: {
        status: 200,
        body: {
          productId: '123',
          available: 10,
          reserved: 2,
        },
      },
    });

    const inventory = await inventoryClient.checkAvailability('123');
    expect(inventory.available).toBe(10);
  });
});

Message Queue Integration

Testing with RabbitMQ/Kafka

describe('OrderProcessor', () => {
  let connection: amqp.Connection;
  let channel: amqp.Channel;

  beforeAll(async () => {
    connection = await amqp.connect(process.env.TEST_RABBITMQ_URL);
    channel = await connection.createChannel();
    await channel.assertQueue('test-orders');
  });

  afterAll(async () => {
    await channel.deleteQueue('test-orders');
    await connection.close();
  });

  it('should process order message', async () => {
    const order = { id: '123', items: [{ productId: '1', quantity: 2 }] };

    // Send message
    channel.sendToQueue('test-orders', Buffer.from(JSON.stringify(order)));

    // Wait for processing
    await new Promise(resolve => setTimeout(resolve, 1000));

    // Verify side effects
    const processedOrder = await prisma.order.findUnique({
      where: { id: '123' },
    });
    expect(processedOrder?.status).toBe('processed');
  });
});

Integration Testing Best Practices

Test Environment Management

// docker-compose.test.yml
version: '3.8'
services:
  test-db:
    image: postgres:15
    environment:
      POSTGRES_DB: test
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5433:5432"

  test-redis:
    image: redis:7
    ports:
      - "6380:6379"
# Start test infrastructure
docker-compose -f docker-compose.test.yml up -d

# Run integration tests
npm run test:integration

# Cleanup
docker-compose -f docker-compose.test.yml down -v

Test Data Management

// factories/userFactory.ts
export const createTestUser = async (overrides = {}) => {
  return prisma.user.create({
    data: {
      name: 'Test User',
      email: `test-${Date.now()}@example.com`,
      passwordHash: await hash('password123'),
      ...overrides,
    },
  });
};

// Usage in tests
const user = await createTestUser({ role: 'admin' });

Parallel Test Execution

// jest.config.js
module.exports = {
  maxWorkers: 4,
  // Each worker gets its own database schema
  globalSetup: './test/setup.js',
  globalTeardown: './test/teardown.js',
};

// setup.js
module.exports = async () => {
  const workerId = process.env.JEST_WORKER_ID;
  process.env.TEST_DATABASE_URL = `postgres://test:test@localhost:5433/test_${workerId}`;
};

Integration Testing Checklist

Before Writing Tests

  • Test environment configured
  • Test database available
  • External services mocked or available
  • Test data factories created

For Each Test

  • Clean state before test
  • Realistic test data
  • Verify both success and failure paths
  • Check side effects (DB, events, etc.)
  • Cleanup after test

CI/CD Considerations

  • Tests run in isolated environment
  • Test infrastructure provisioned automatically
  • Reasonable timeout settings
  • Parallel execution configured
  • Test reports generated


Compliance

This section fulfills ISO 13485 requirements for design verification (7.3.6) and process validation (7.5.6), and ISO 27001 requirements for secure development lifecycle (A.8.25) and secure architecture verification (A.8.27).

View full compliance matrix

How is this guide?

Last updated on

On this page