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
- Use test database - Never test against production
- Clean up test data - Use beforeEach/afterEach
- Isolate tests - Each test should be independent
- Mock external services - Don't make real API calls
- Test error cases - What happens when things fail
- Test edge cases - Boundary conditions
- 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();
});