Integration tests verify that different parts of your system work together correctly. They catch issues that unit tests miss—configuration problems, database interactions, and API contracts.
Unit vs Integration Tests#
Unit Tests:
- Test single functions/classes
- Mock all dependencies
- Fast (< 10ms each)
- Run frequently
Integration Tests:
- Test multiple components together
- Real databases, APIs, services
- Slower (100ms - 5s each)
- Run before deployment
API Integration Tests#
1import supertest from 'supertest';
2import { app } from '../app';
3import { prisma } from '../lib/prisma';
4
5const request = supertest(app);
6
7describe('POST /api/users', () => {
8 beforeEach(async () => {
9 // Clean database before each test
10 await prisma.user.deleteMany();
11 });
12
13 afterAll(async () => {
14 await prisma.$disconnect();
15 });
16
17 it('should create a new user', async () => {
18 const response = await request
19 .post('/api/users')
20 .send({
21 email: 'test@example.com',
22 name: 'Test User',
23 password: 'securePassword123',
24 })
25 .expect(201);
26
27 expect(response.body).toMatchObject({
28 id: expect.any(String),
29 email: 'test@example.com',
30 name: 'Test User',
31 });
32 expect(response.body).not.toHaveProperty('password');
33
34 // Verify in database
35 const user = await prisma.user.findUnique({
36 where: { email: 'test@example.com' },
37 });
38 expect(user).not.toBeNull();
39 });
40
41 it('should return 400 for invalid email', async () => {
42 const response = await request
43 .post('/api/users')
44 .send({
45 email: 'invalid-email',
46 name: 'Test User',
47 password: 'securePassword123',
48 })
49 .expect(400);
50
51 expect(response.body.error.code).toBe('VALIDATION_ERROR');
52 });
53
54 it('should return 409 for duplicate email', async () => {
55 // Create first user
56 await request
57 .post('/api/users')
58 .send({
59 email: 'test@example.com',
60 name: 'First User',
61 password: 'password123',
62 });
63
64 // Try to create duplicate
65 const response = await request
66 .post('/api/users')
67 .send({
68 email: 'test@example.com',
69 name: 'Second User',
70 password: 'password456',
71 })
72 .expect(409);
73
74 expect(response.body.error.code).toBe('EMAIL_EXISTS');
75 });
76});Database Integration Tests#
1import { prisma } from '../lib/prisma';
2import { createOrder, getOrderWithItems } from '../services/order';
3
4describe('Order Service', () => {
5 let testUser: User;
6
7 beforeAll(async () => {
8 // Create test user
9 testUser = await prisma.user.create({
10 data: {
11 email: 'test@example.com',
12 name: 'Test User',
13 },
14 });
15 });
16
17 beforeEach(async () => {
18 // Clean orders between tests
19 await prisma.orderItem.deleteMany();
20 await prisma.order.deleteMany();
21 });
22
23 afterAll(async () => {
24 await prisma.user.deleteMany();
25 await prisma.$disconnect();
26 });
27
28 it('should create order with items in a transaction', async () => {
29 const items = [
30 { productId: 'prod-1', quantity: 2, price: 1000 },
31 { productId: 'prod-2', quantity: 1, price: 500 },
32 ];
33
34 const order = await createOrder(testUser.id, items);
35
36 expect(order.total).toBe(2500);
37 expect(order.items).toHaveLength(2);
38
39 // Verify database state
40 const dbOrder = await prisma.order.findUnique({
41 where: { id: order.id },
42 include: { items: true },
43 });
44
45 expect(dbOrder?.items).toHaveLength(2);
46 expect(dbOrder?.total).toBe(2500);
47 });
48
49 it('should rollback transaction on failure', async () => {
50 const items = [
51 { productId: 'prod-1', quantity: 2, price: 1000 },
52 { productId: 'invalid', quantity: -1, price: 500 }, // Invalid
53 ];
54
55 await expect(createOrder(testUser.id, items)).rejects.toThrow();
56
57 // Verify nothing was created
58 const orders = await prisma.order.findMany({
59 where: { userId: testUser.id },
60 });
61 expect(orders).toHaveLength(0);
62 });
63});Test Database Setup#
1// test/setup.ts
2import { execSync } from 'child_process';
3import { prisma } from '../lib/prisma';
4
5beforeAll(async () => {
6 // Use test database
7 process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
8
9 // Reset database schema
10 execSync('npx prisma db push --force-reset', {
11 env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL },
12 });
13});
14
15afterAll(async () => {
16 await prisma.$disconnect();
17});
18
19// jest.config.js
20module.exports = {
21 setupFilesAfterEnv: ['./test/setup.ts'],
22 testEnvironment: 'node',
23 testMatch: ['**/*.integration.test.ts'],
24};External Service Mocking#
1import nock from 'nock';
2import { PaymentService } from '../services/payment';
3
4describe('PaymentService', () => {
5 beforeEach(() => {
6 nock.cleanAll();
7 });
8
9 it('should process payment successfully', async () => {
10 // Mock Stripe API
11 nock('https://api.stripe.com')
12 .post('/v1/charges')
13 .reply(200, {
14 id: 'ch_123',
15 status: 'succeeded',
16 amount: 1000,
17 });
18
19 const payment = new PaymentService();
20 const result = await payment.charge({
21 amount: 1000,
22 currency: 'usd',
23 source: 'tok_visa',
24 });
25
26 expect(result.status).toBe('succeeded');
27 });
28
29 it('should handle payment failure', async () => {
30 nock('https://api.stripe.com')
31 .post('/v1/charges')
32 .reply(402, {
33 error: {
34 type: 'card_error',
35 message: 'Card declined',
36 },
37 });
38
39 const payment = new PaymentService();
40
41 await expect(
42 payment.charge({ amount: 1000, currency: 'usd', source: 'tok_declined' })
43 ).rejects.toThrow('Card declined');
44 });
45
46 it('should retry on network failure', async () => {
47 // First two calls fail, third succeeds
48 nock('https://api.stripe.com')
49 .post('/v1/charges')
50 .replyWithError('Network error')
51 .post('/v1/charges')
52 .replyWithError('Network error')
53 .post('/v1/charges')
54 .reply(200, { id: 'ch_123', status: 'succeeded' });
55
56 const payment = new PaymentService({ retries: 3 });
57 const result = await payment.charge({
58 amount: 1000,
59 currency: 'usd',
60 source: 'tok_visa',
61 });
62
63 expect(result.status).toBe('succeeded');
64 });
65});Docker Test Environment#
1# docker-compose.test.yml
2version: '3.8'
3
4services:
5 test-db:
6 image: postgres:15
7 environment:
8 POSTGRES_USER: test
9 POSTGRES_PASSWORD: test
10 POSTGRES_DB: test_db
11 ports:
12 - '5433:5432'
13 tmpfs:
14 - /var/lib/postgresql/data
15
16 test-redis:
17 image: redis:7
18 ports:
19 - '6380:6379'# Run tests with Docker
docker-compose -f docker-compose.test.yml up -d
npm run test:integration
docker-compose -f docker-compose.test.yml downTest Fixtures#
1// fixtures/users.ts
2export const fixtures = {
3 users: {
4 admin: {
5 id: 'user-admin',
6 email: 'admin@example.com',
7 name: 'Admin User',
8 role: 'admin',
9 },
10 regular: {
11 id: 'user-regular',
12 email: 'user@example.com',
13 name: 'Regular User',
14 role: 'user',
15 },
16 },
17 products: {
18 laptop: {
19 id: 'prod-laptop',
20 name: 'Laptop',
21 price: 99900,
22 stock: 10,
23 },
24 mouse: {
25 id: 'prod-mouse',
26 name: 'Mouse',
27 price: 2900,
28 stock: 50,
29 },
30 },
31};
32
33// Load fixtures
34async function loadFixtures() {
35 await prisma.user.createMany({
36 data: Object.values(fixtures.users),
37 });
38 await prisma.product.createMany({
39 data: Object.values(fixtures.products),
40 });
41}CI Pipeline#
1# .github/workflows/test.yml
2name: Integration Tests
3
4on: [push, pull_request]
5
6jobs:
7 integration-tests:
8 runs-on: ubuntu-latest
9
10 services:
11 postgres:
12 image: postgres:15
13 env:
14 POSTGRES_USER: test
15 POSTGRES_PASSWORD: test
16 POSTGRES_DB: test_db
17 ports:
18 - 5432:5432
19 options: >-
20 --health-cmd pg_isready
21 --health-interval 10s
22 --health-timeout 5s
23 --health-retries 5
24
25 steps:
26 - uses: actions/checkout@v4
27
28 - name: Setup Node.js
29 uses: actions/setup-node@v4
30 with:
31 node-version: '20'
32 cache: 'npm'
33
34 - name: Install dependencies
35 run: npm ci
36
37 - name: Run migrations
38 run: npx prisma db push
39 env:
40 DATABASE_URL: postgresql://test:test@localhost:5432/test_db
41
42 - name: Run integration tests
43 run: npm run test:integration
44 env:
45 DATABASE_URL: postgresql://test:test@localhost:5432/test_dbConclusion#
Integration tests are your safety net for system behavior. They catch real bugs that unit tests miss—database constraints, API contracts, and configuration issues.
Invest in a fast, reliable test database setup, and run integration tests before every deployment.