Skip to main content

Integration Testing

Integration tests verify that multiple components work together correctly, including API endpoints, database interactions, and external service integrations.

What to Test

✅ DO Test

  • API endpoints (request/response)
  • Database queries and transactions
  • Service integrations (with mocks or test mode)
  • Authentication flows
  • Authorization checks
  • Error handling across layers

❌ DON'T Test

  • Third-party API responses (mock them)
  • Actual external service calls in CI (use test mode)
  • UI rendering (test in E2E)

Writing Integration Tests

Example: Testing an API Endpoint

// tests/integration/api/users.test.js
import request from 'supertest';
import app from '../../src/server.js';
import { sql } from '../../src/server.js';

describe('POST /api/users', () => {
beforeEach(async () => {
// Clean up test data
await sql`DELETE FROM users WHERE email LIKE 'test-%'`;
});

afterAll(async () => {
await sql`DELETE FROM users WHERE email LIKE 'test-%'`;
});

test('creates new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'test-user@example.com',
name: 'Test User',
password: 'SecurePassword123!'
})
.expect(201);

expect(response.body).toMatchObject({
id: expect.any(String),
email: 'test-user@example.com',
name: 'Test User'
});
expect(response.body.password).toBeUndefined(); // Never return password
});

test('rejects duplicate email', async () => {
// Create first user
await request(app)
.post('/api/users')
.send({
email: 'test-duplicate@example.com',
name: 'First User',
password: 'Password123!'
});

// Try to create duplicate
const response = await request(app)
.post('/api/users')
.send({
email: 'test-duplicate@example.com',
name: 'Second User',
password: 'Password123!'
})
.expect(400);

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

test('validates required fields', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'Test User'
// Missing email and password
})
.expect(400);

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

Example: Testing Database Integration

// tests/integration/database/users.test.js
import { sql } from '../../src/server.js';

describe('User Database Operations', () => {
let testUserId;

beforeEach(async () => {
// Create test user
const [user] = await sql`
INSERT INTO users (email, name, password_hash)
VALUES ('test-db@example.com', 'Test User', 'hashed-password')
RETURNING id
`;
testUserId = user.id;
});

afterEach(async () => {
// Clean up
await sql`DELETE FROM users WHERE id = ${testUserId}`;
});

test('finds user by email', async () => {
const [user] = await sql`
SELECT * FROM users WHERE email = 'test-db@example.com'
`;

expect(user).toBeDefined();
expect(user.email).toBe('test-db@example.com');
});

test('updates user', async () => {
await sql`
UPDATE users
SET name = 'Updated Name'
WHERE id = ${testUserId}
`;

const [user] = await sql`
SELECT * FROM users WHERE id = ${testUserId}
`;

expect(user.name).toBe('Updated Name');
});
});

Test Database Setup

Using Test Database

// tests/setup.js
import { sql } from '../src/server.js';

beforeAll(async () => {
// Use test database
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
});

afterAll(async () => {
// Clean up
await sql.end();
});

Database Migrations in Tests

// Run migrations before tests
beforeAll(async () => {
await runMigrations(sql);
});

Mocking External Services

Example: Mocking External API

// tests/integration/services/email-service.test.js
import { sendEmail } from '../../src/services/email-service.js';
import * as emailProvider from '../../src/services/email-provider.js';

jest.mock('../../src/services/email-provider.js');

describe('Email Service Integration', () => {
test('sends email through provider', async () => {
emailProvider.sendEmail.mockResolvedValue({ success: true });

const result = await sendEmail({
to: 'test@example.com',
subject: 'Test',
body: 'Test body'
});

expect(emailProvider.sendEmail).toHaveBeenCalled();
expect(result.success).toBe(true);
});
});

Authentication Testing

Example: Testing Protected Endpoints

// tests/integration/api/protected.test.js
import request from 'supertest';
import app from '../../src/server.js';

describe('Protected Endpoints', () => {
let authToken;

beforeAll(async () => {
// Get auth token
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
authToken = response.body.token;
});

test('allows access with valid token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);

expect(response.body).toBeDefined();
});

test('rejects access without token', async () => {
await request(app)
.get('/api/protected')
.expect(401);
});

test('rejects access with invalid token', async () => {
await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});

Running Integration Tests

# Run all integration tests
npm run test:integration

# Run specific test file
npm run test:integration -- api/users.test.js

# Run with test database
TEST_DATABASE_URL=postgresql://... npm run test:integration

Best Practices

  1. Use test database - Never test against production
  2. Clean up test data - Use beforeEach/afterEach
  3. Isolate tests - Each test should be independent
  4. Mock external services - Don't make real API calls
  5. Test error cases - What happens when things fail
  6. Test edge cases - Boundary conditions
  7. Use realistic data - Test with production-like data

Common Patterns

Testing Transactions

test('rolls back on error', async () => {
await sql.begin(async sql => {
await sql`INSERT INTO users ...`;
throw new Error('Test error');
// Transaction should rollback
});

const users = await sql`SELECT * FROM users WHERE ...`;
expect(users).toHaveLength(0);
});

Testing Webhooks

test('handles webhook correctly', async () => {
const webhookPayload = {
event: 'user.created',
data: { id: '123', email: 'test@example.com' }
};

const response = await request(app)
.post('/webhooks/stripe/secret')
.send(webhookPayload)
.expect(200);

// Verify database was updated
const user = await sql`SELECT * FROM users WHERE id = '123'`;
expect(user).toBeDefined();
});

Resources