Back to Blog
TestingIntegration TestsAPI TestingDatabase

Integration Testing: Testing Components Together

Write effective integration tests. Learn database testing, API testing, and patterns for testing component interactions.

B
Bootspring Team
Engineering
February 27, 2026
5 min read

Integration tests verify that components work correctly together. This guide covers patterns for testing real interactions.

API Integration Tests#

Testing Express Routes#

1import request from 'supertest'; 2import { app } from '../app'; 3import { db } from '../db'; 4 5describe('POST /api/users', () => { 6 beforeEach(async () => { 7 await db.users.deleteMany(); 8 }); 9 10 it('should create a new user', async () => { 11 const response = await request(app) 12 .post('/api/users') 13 .send({ 14 email: 'test@example.com', 15 name: 'Test User', 16 password: 'securePassword123', 17 }) 18 .expect(201); 19 20 expect(response.body).toMatchObject({ 21 email: 'test@example.com', 22 name: 'Test User', 23 }); 24 expect(response.body).not.toHaveProperty('password'); 25 26 // Verify in database 27 const user = await db.users.findByEmail('test@example.com'); 28 expect(user).toBeTruthy(); 29 }); 30 31 it('should return 400 for invalid email', async () => { 32 const response = await request(app) 33 .post('/api/users') 34 .send({ 35 email: 'invalid-email', 36 name: 'Test', 37 password: 'password123', 38 }) 39 .expect(400); 40 41 expect(response.body.error).toContain('email'); 42 }); 43 44 it('should return 409 for duplicate email', async () => { 45 // Create first user 46 await request(app) 47 .post('/api/users') 48 .send({ 49 email: 'test@example.com', 50 name: 'First User', 51 password: 'password123', 52 }); 53 54 // Try to create duplicate 55 const response = await request(app) 56 .post('/api/users') 57 .send({ 58 email: 'test@example.com', 59 name: 'Second User', 60 password: 'password456', 61 }) 62 .expect(409); 63 64 expect(response.body.error).toContain('already exists'); 65 }); 66});

Testing Authentication#

1describe('Protected Routes', () => { 2 let authToken: string; 3 4 beforeAll(async () => { 5 // Create test user and get token 6 await request(app) 7 .post('/api/users') 8 .send({ email: 'auth@test.com', password: 'password123' }); 9 10 const loginResponse = await request(app) 11 .post('/api/auth/login') 12 .send({ email: 'auth@test.com', password: 'password123' }); 13 14 authToken = loginResponse.body.token; 15 }); 16 17 it('should access protected route with valid token', async () => { 18 const response = await request(app) 19 .get('/api/profile') 20 .set('Authorization', `Bearer ${authToken}`) 21 .expect(200); 22 23 expect(response.body.email).toBe('auth@test.com'); 24 }); 25 26 it('should reject request without token', async () => { 27 await request(app) 28 .get('/api/profile') 29 .expect(401); 30 }); 31 32 it('should reject request with invalid token', async () => { 33 await request(app) 34 .get('/api/profile') 35 .set('Authorization', 'Bearer invalid-token') 36 .expect(401); 37 }); 38});

Database Integration Tests#

Using Test Containers#

1import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; 2import { PrismaClient } from '@prisma/client'; 3 4describe('UserRepository', () => { 5 let container: StartedPostgreSqlContainer; 6 let prisma: PrismaClient; 7 8 beforeAll(async () => { 9 // Start PostgreSQL container 10 container = await new PostgreSqlContainer() 11 .withDatabase('test') 12 .start(); 13 14 // Connect Prisma to test database 15 prisma = new PrismaClient({ 16 datasources: { 17 db: { url: container.getConnectionUri() }, 18 }, 19 }); 20 21 // Run migrations 22 await prisma.$executeRawUnsafe(` 23 CREATE TABLE users ( 24 id SERIAL PRIMARY KEY, 25 email VARCHAR(255) UNIQUE NOT NULL, 26 name VARCHAR(255) 27 ) 28 `); 29 }); 30 31 afterAll(async () => { 32 await prisma.$disconnect(); 33 await container.stop(); 34 }); 35 36 beforeEach(async () => { 37 await prisma.user.deleteMany(); 38 }); 39 40 it('should create and retrieve user', async () => { 41 const created = await prisma.user.create({ 42 data: { email: 'test@example.com', name: 'Test' }, 43 }); 44 45 const found = await prisma.user.findUnique({ 46 where: { id: created.id }, 47 }); 48 49 expect(found).toMatchObject({ 50 email: 'test@example.com', 51 name: 'Test', 52 }); 53 }); 54});

Transaction Testing#

1describe('Order Processing', () => { 2 it('should rollback on payment failure', async () => { 3 const user = await createTestUser(); 4 const product = await createTestProduct({ inventory: 10 }); 5 6 // Mock payment to fail 7 paymentService.charge.mockRejectedValue(new Error('Payment declined')); 8 9 await expect( 10 orderService.createOrder({ 11 userId: user.id, 12 items: [{ productId: product.id, quantity: 2 }], 13 }) 14 ).rejects.toThrow('Payment declined'); 15 16 // Verify inventory wasn't deducted 17 const updatedProduct = await db.products.findById(product.id); 18 expect(updatedProduct.inventory).toBe(10); 19 20 // Verify no order was created 21 const orders = await db.orders.findByUser(user.id); 22 expect(orders).toHaveLength(0); 23 }); 24});

External Service Integration#

Using MSW for API Mocking#

1import { setupServer } from 'msw/node'; 2import { http, HttpResponse } from 'msw'; 3 4const server = setupServer( 5 http.get('https://api.stripe.com/v1/customers/:id', ({ params }) => { 6 return HttpResponse.json({ 7 id: params.id, 8 email: 'customer@example.com', 9 name: 'Test Customer', 10 }); 11 }), 12 13 http.post('https://api.stripe.com/v1/charges', async ({ request }) => { 14 const body = await request.json(); 15 16 if (body.amount > 100000) { 17 return HttpResponse.json( 18 { error: { message: 'Amount too large' } }, 19 { status: 400 } 20 ); 21 } 22 23 return HttpResponse.json({ 24 id: 'ch_test123', 25 amount: body.amount, 26 status: 'succeeded', 27 }); 28 }) 29); 30 31beforeAll(() => server.listen()); 32afterEach(() => server.resetHandlers()); 33afterAll(() => server.close()); 34 35describe('PaymentService', () => { 36 it('should process payment successfully', async () => { 37 const result = await paymentService.charge({ 38 customerId: 'cus_123', 39 amount: 5000, 40 }); 41 42 expect(result.status).toBe('succeeded'); 43 }); 44 45 it('should handle large amount error', async () => { 46 await expect( 47 paymentService.charge({ 48 customerId: 'cus_123', 49 amount: 200000, 50 }) 51 ).rejects.toThrow('Amount too large'); 52 }); 53});

Redis Integration#

1import Redis from 'ioredis'; 2import { GenericContainer } from 'testcontainers'; 3 4describe('CacheService', () => { 5 let redis: Redis; 6 let container: StartedTestContainer; 7 8 beforeAll(async () => { 9 container = await new GenericContainer('redis:7-alpine') 10 .withExposedPorts(6379) 11 .start(); 12 13 redis = new Redis({ 14 host: container.getHost(), 15 port: container.getMappedPort(6379), 16 }); 17 }); 18 19 afterAll(async () => { 20 await redis.quit(); 21 await container.stop(); 22 }); 23 24 beforeEach(async () => { 25 await redis.flushall(); 26 }); 27 28 it('should cache and retrieve values', async () => { 29 const cacheService = new CacheService(redis); 30 31 await cacheService.set('user:1', { name: 'John' }, 3600); 32 const cached = await cacheService.get('user:1'); 33 34 expect(cached).toEqual({ name: 'John' }); 35 }); 36 37 it('should return null for expired keys', async () => { 38 const cacheService = new CacheService(redis); 39 40 await cacheService.set('temp', 'value', 1); // 1 second TTL 41 42 await new Promise(resolve => setTimeout(resolve, 1100)); 43 44 const result = await cacheService.get('temp'); 45 expect(result).toBeNull(); 46 }); 47});

Best Practices#

  1. Use real databases in containers: More realistic than mocks
  2. Clean up between tests: Prevent test pollution
  3. Test failure scenarios: Network errors, timeouts, invalid data
  4. Keep tests focused: One integration point per test
  5. Use factories for test data: Consistent, maintainable setup

Conclusion#

Integration tests verify real interactions between components. Use test containers for databases, MSW for external APIs, and ensure proper cleanup between tests. Balance coverage with test execution time.

Share this article

Help spread the word about Bootspring