Integration Testing Pattern
Test complete user flows and service interactions with database integration, API testing, and webhook verification.
Overview#
Integration tests verify that different parts of your application work together correctly. They test real database interactions, API endpoints, and service integrations to ensure the system functions as a whole.
When to use:
- Testing complete user flows (registration, checkout)
- Verifying database operations work correctly
- Testing API endpoints with real data
- Validating webhook handling
Key features:
- Tests real database interactions
- Verifies service integrations
- Tests complete user journeys
- Catches integration bugs
Code Example#
Database Integration Tests#
1// __tests__/integration/setup.ts
2import { PrismaClient } from '@prisma/client'
3import { execSync } from 'child_process'
4
5const prisma = new PrismaClient()
6
7export async function setupTestDatabase() {
8 // Reset database before tests
9 await prisma.$executeRawUnsafe(`
10 TRUNCATE TABLE "User", "Post", "Comment" CASCADE;
11 `)
12}
13
14export async function teardownTestDatabase() {
15 await prisma.$disconnect()
16}
17
18// Or use test containers
19import { PostgreSqlContainer } from '@testcontainers/postgresql'
20
21let container: PostgreSqlContainer
22
23export async function startTestContainer() {
24 container = await new PostgreSqlContainer()
25 .withDatabase('testdb')
26 .withUsername('test')
27 .withPassword('test')
28 .start()
29
30 process.env.DATABASE_URL = container.getConnectionUri()
31
32 // Run migrations
33 execSync('npx prisma migrate deploy', {
34 env: { ...process.env, DATABASE_URL: container.getConnectionUri() }
35 })
36}
37
38export async function stopTestContainer() {
39 await container?.stop()
40}User Flow Integration Test#
1// __tests__/integration/user-flow.test.ts
2import { prisma } from '@/lib/db'
3import { POST as createUser } from '@/app/api/users/route'
4import { POST as login } from '@/app/api/auth/login/route'
5import { POST as createPost } from '@/app/api/posts/route'
6
7describe('User Flow Integration', () => {
8 beforeEach(async () => {
9 await prisma.post.deleteMany()
10 await prisma.user.deleteMany()
11 })
12
13 afterAll(async () => {
14 await prisma.$disconnect()
15 })
16
17 it('completes full user registration and posting flow', async () => {
18 // Step 1: Register user
19 const registerRequest = new Request('http://localhost/api/users', {
20 method: 'POST',
21 headers: { 'Content-Type': 'application/json' },
22 body: JSON.stringify({
23 email: 'test@example.com',
24 password: 'password123',
25 name: 'Test User'
26 })
27 })
28
29 const registerResponse = await createUser(registerRequest)
30 expect(registerResponse.status).toBe(201)
31
32 const user = await registerResponse.json()
33 expect(user.email).toBe('test@example.com')
34
35 // Step 2: Login
36 const loginRequest = new Request('http://localhost/api/auth/login', {
37 method: 'POST',
38 headers: { 'Content-Type': 'application/json' },
39 body: JSON.stringify({
40 email: 'test@example.com',
41 password: 'password123'
42 })
43 })
44
45 const loginResponse = await login(loginRequest)
46 expect(loginResponse.status).toBe(200)
47
48 const { token } = await loginResponse.json()
49 expect(token).toBeDefined()
50
51 // Step 3: Create post
52 const postRequest = new Request('http://localhost/api/posts', {
53 method: 'POST',
54 headers: {
55 'Content-Type': 'application/json',
56 Authorization: `Bearer ${token}`
57 },
58 body: JSON.stringify({
59 title: 'My First Post',
60 content: 'Hello world!'
61 })
62 })
63
64 const postResponse = await createPost(postRequest)
65 expect(postResponse.status).toBe(201)
66
67 const post = await postResponse.json()
68 expect(post.title).toBe('My First Post')
69 expect(post.authorId).toBe(user.id)
70
71 // Verify in database
72 const dbPost = await prisma.post.findUnique({
73 where: { id: post.id },
74 include: { author: true }
75 })
76
77 expect(dbPost?.author.email).toBe('test@example.com')
78 })
79})API Integration Tests with Supertest#
1// __tests__/integration/api.test.ts
2import request from 'supertest'
3import { createServer } from 'http'
4import { parse } from 'url'
5import next from 'next'
6
7const dev = process.env.NODE_ENV !== 'production'
8const app = next({ dev })
9const handle = app.getRequestHandler()
10
11let server: ReturnType<typeof createServer>
12
13beforeAll(async () => {
14 await app.prepare()
15 server = createServer((req, res) => {
16 const parsedUrl = parse(req.url!, true)
17 handle(req, res, parsedUrl)
18 })
19 await new Promise<void>(resolve => server.listen(0, resolve))
20})
21
22afterAll(async () => {
23 await new Promise<void>(resolve => server.close(() => resolve()))
24 await app.close()
25})
26
27describe('API Integration Tests', () => {
28 it('GET /api/health returns healthy status', async () => {
29 const res = await request(server)
30 .get('/api/health')
31 .expect(200)
32
33 expect(res.body.status).toBe('healthy')
34 })
35
36 it('POST /api/users creates a new user', async () => {
37 const res = await request(server)
38 .post('/api/users')
39 .send({
40 email: 'new@example.com',
41 name: 'New User',
42 password: 'password123'
43 })
44 .expect(201)
45
46 expect(res.body.email).toBe('new@example.com')
47 expect(res.body.password).toBeUndefined()
48 })
49
50 it('GET /api/users requires authentication', async () => {
51 await request(server)
52 .get('/api/users')
53 .expect(401)
54 })
55})Service Integration Tests#
1// __tests__/integration/services.test.ts
2import { PaymentService } from '@/services/payment'
3import { EmailService } from '@/services/email'
4import { prisma } from '@/lib/db'
5
6// Mock external services
7vi.mock('@/services/email', () => ({
8 EmailService: {
9 sendWelcomeEmail: vi.fn().mockResolvedValue(true),
10 sendReceiptEmail: vi.fn().mockResolvedValue(true)
11 }
12}))
13
14describe('Payment Service Integration', () => {
15 beforeEach(async () => {
16 await prisma.payment.deleteMany()
17 await prisma.subscription.deleteMany()
18 vi.clearAllMocks()
19 })
20
21 it('processes subscription and sends confirmation', async () => {
22 const user = await prisma.user.create({
23 data: {
24 email: 'subscriber@example.com',
25 name: 'Subscriber'
26 }
27 })
28
29 // Process subscription
30 const subscription = await PaymentService.createSubscription({
31 userId: user.id,
32 planId: 'pro',
33 paymentMethodId: 'pm_test_123'
34 })
35
36 expect(subscription.status).toBe('active')
37 expect(subscription.planId).toBe('pro')
38
39 // Verify database state
40 const dbSubscription = await prisma.subscription.findUnique({
41 where: { id: subscription.id }
42 })
43 expect(dbSubscription?.userId).toBe(user.id)
44
45 // Verify email was sent
46 expect(EmailService.sendReceiptEmail).toHaveBeenCalledWith(
47 expect.objectContaining({
48 email: 'subscriber@example.com',
49 subscriptionId: subscription.id
50 })
51 )
52 })
53
54 it('handles failed payment gracefully', async () => {
55 const user = await prisma.user.create({
56 data: {
57 email: 'fail@example.com',
58 name: 'Failed User'
59 }
60 })
61
62 await expect(
63 PaymentService.createSubscription({
64 userId: user.id,
65 planId: 'pro',
66 paymentMethodId: 'pm_card_declined'
67 })
68 ).rejects.toThrow('Payment declined')
69
70 // Verify no subscription was created
71 const subscriptions = await prisma.subscription.findMany({
72 where: { userId: user.id }
73 })
74 expect(subscriptions).toHaveLength(0)
75 })
76})Webhook Integration Tests#
1// __tests__/integration/webhooks.test.ts
2import crypto from 'crypto'
3import { POST } from '@/app/api/webhooks/stripe/route'
4import { prisma } from '@/lib/db'
5
6function createStripeSignature(payload: string, secret: string): string {
7 const timestamp = Math.floor(Date.now() / 1000)
8 const signedPayload = `${timestamp}.${payload}`
9 const signature = crypto
10 .createHmac('sha256', secret)
11 .update(signedPayload)
12 .digest('hex')
13
14 return `t=${timestamp},v1=${signature}`
15}
16
17describe('Stripe Webhook Integration', () => {
18 const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
19
20 beforeEach(async () => {
21 await prisma.payment.deleteMany()
22 })
23
24 it('handles payment_intent.succeeded event', async () => {
25 const user = await prisma.user.create({
26 data: {
27 email: 'payer@example.com',
28 name: 'Payer',
29 stripeCustomerId: 'cus_test_123'
30 }
31 })
32
33 const event = {
34 id: 'evt_test_123',
35 type: 'payment_intent.succeeded',
36 data: {
37 object: {
38 id: 'pi_test_123',
39 amount: 2000,
40 currency: 'usd',
41 customer: 'cus_test_123',
42 metadata: { userId: user.id }
43 }
44 }
45 }
46
47 const payload = JSON.stringify(event)
48 const signature = createStripeSignature(payload, webhookSecret)
49
50 const request = new Request('http://localhost/api/webhooks/stripe', {
51 method: 'POST',
52 headers: {
53 'Content-Type': 'application/json',
54 'stripe-signature': signature
55 },
56 body: payload
57 })
58
59 const response = await POST(request)
60 expect(response.status).toBe(200)
61
62 // Verify payment was recorded
63 const payment = await prisma.payment.findFirst({
64 where: { stripePaymentIntentId: 'pi_test_123' }
65 })
66
67 expect(payment).toBeDefined()
68 expect(payment?.amount).toBe(2000)
69 expect(payment?.status).toBe('succeeded')
70 })
71
72 it('rejects invalid signatures', async () => {
73 const event = {
74 id: 'evt_test_456',
75 type: 'payment_intent.succeeded',
76 data: { object: {} }
77 }
78
79 const request = new Request('http://localhost/api/webhooks/stripe', {
80 method: 'POST',
81 headers: {
82 'Content-Type': 'application/json',
83 'stripe-signature': 'invalid_signature'
84 },
85 body: JSON.stringify(event)
86 })
87
88 const response = await POST(request)
89 expect(response.status).toBe(400)
90 })
91})Database Transaction Tests#
1// __tests__/integration/transactions.test.ts
2import { prisma } from '@/lib/db'
3import { transferFunds } from '@/services/banking'
4
5describe('Database Transactions', () => {
6 beforeEach(async () => {
7 await prisma.account.deleteMany()
8 })
9
10 it('transfers funds atomically', async () => {
11 const [sender, receiver] = await Promise.all([
12 prisma.account.create({ data: { userId: 'user1', balance: 100 } }),
13 prisma.account.create({ data: { userId: 'user2', balance: 0 } })
14 ])
15
16 await transferFunds(sender.id, receiver.id, 50)
17
18 const [updatedSender, updatedReceiver] = await Promise.all([
19 prisma.account.findUnique({ where: { id: sender.id } }),
20 prisma.account.findUnique({ where: { id: receiver.id } })
21 ])
22
23 expect(updatedSender?.balance).toBe(50)
24 expect(updatedReceiver?.balance).toBe(50)
25 })
26
27 it('rolls back on insufficient funds', async () => {
28 const [sender, receiver] = await Promise.all([
29 prisma.account.create({ data: { userId: 'user1', balance: 30 } }),
30 prisma.account.create({ data: { userId: 'user2', balance: 0 } })
31 ])
32
33 await expect(transferFunds(sender.id, receiver.id, 50))
34 .rejects.toThrow('Insufficient funds')
35
36 // Verify no changes were made
37 const [unchangedSender, unchangedReceiver] = await Promise.all([
38 prisma.account.findUnique({ where: { id: sender.id } }),
39 prisma.account.findUnique({ where: { id: receiver.id } })
40 ])
41
42 expect(unchangedSender?.balance).toBe(30)
43 expect(unchangedReceiver?.balance).toBe(0)
44 })
45})Usage Instructions#
- Set up test database: Use separate database or test containers
- Clean between tests: Reset database state in
beforeEach - Test real interactions: Use actual database and API calls
- Mock external services: Mock third-party APIs but test your integration code
- Verify side effects: Check database state, emails sent, etc.
Best Practices#
- Isolate tests - Each test should start with clean state
- Use transactions - Wrap tests in transactions for easy rollback
- Test happy and error paths - Verify both success and failure scenarios
- Mock external services - Don't call real third-party APIs in tests
- Use realistic data - Test with data similar to production
- Test complete flows - Verify entire user journeys
- Run in CI - Ensure integration tests run in your pipeline
Related Patterns#
- Vitest - Test runner configuration
- Fixtures - Test data management
- Mocking - Mocking external services
- E2E Testing - Full end-to-end testing