Back to Blog
TestingIntegration TestsAPI TestingDatabases

Integration Testing Strategies for Modern Applications

Test how components work together. From API testing to database integration to external service mocking.

B
Bootspring Team
Engineering
May 20, 2024
5 min read

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 down

Test 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_db

Conclusion#

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.

Share this article

Help spread the word about Bootspring