Prerequisites
- Unit testing fundamentals 📝
- Jest or similar testing framework ⚡
- Basic TypeScript knowledge 💻
- Understanding of mocking concepts 🎭
What you'll learn
- Understand integration testing principles 🎯
- Write effective integration tests for TypeScript applications 🏗️
- Test interactions between multiple components 🔗
- Handle external dependencies in integration tests 🌐
- Debug complex integration test scenarios 🐛
🎯 Introduction
Welcome to the exciting world of integration testing! 🎉 While unit tests verify individual components work correctly, integration tests ensure these components play nicely together - like making sure your orchestra doesn’t sound like a bunch of cats fighting! 🎵
Integration testing bridges the gap between isolated unit tests and full end-to-end tests. You’ll discover how to test the interactions between services, databases, APIs, and other components that make your TypeScript applications tick.
By the end of this tutorial, you’ll be confidently writing integration tests that catch those tricky bugs that only appear when components interact! Let’s dive in! 🏊♂️
📚 Understanding Integration Testing
🤔 What is Integration Testing?
Integration testing is like testing a recipe by actually cooking it! 🍳 While unit tests check if each ingredient is good individually, integration tests verify the final dish tastes amazing when everything comes together.
In TypeScript applications, integration testing means:
- ✨ Testing how multiple modules interact
- 🚀 Verifying data flows between components
- 🛡️ Ensuring external service integrations work
- 📊 Testing complete user workflows
💡 Why Integration Testing Matters
Here’s why developers swear by integration tests:
- Catch Interface Bugs 🔍: Find issues between components
- Verify Real Workflows 📋: Test actual user scenarios
- Database Integration 🗄️: Ensure data persistence works
- API Communication 🌐: Test external service calls
- Configuration Issues ⚙️: Catch environment-specific problems
Real-world example: Your user service and email service work perfectly alone, but integration tests reveal that user registration emails contain undefined values! 😱
🔧 Integration Testing Setup
📝 Basic Test Environment
Let’s create a solid foundation for integration testing:
// 🏗️ test/setup/integration-setup.ts
import { MongoMemoryServer } from 'mongodb-memory-server';
import mongoose from 'mongoose';
import { app } from '../../src/app';
export class IntegrationTestSetup {
private mongoServer: MongoMemoryServer | null = null;
// 🚀 Setup test environment
async setup(): Promise<void> {
// 💾 Start in-memory MongoDB
this.mongoServer = await MongoMemoryServer.create();
const mongoUri = this.mongoServer.getUri();
// 🔗 Connect to test database
await mongoose.connect(mongoUri);
console.log('🎯 Integration test environment ready!');
}
// 🧹 Cleanup after tests
async cleanup(): Promise<void> {
if (mongoose.connection.readyState !== 0) {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
}
if (this.mongoServer) {
await this.mongoServer.stop();
}
console.log('✨ Integration test cleanup complete!');
}
// 🔄 Reset between tests
async reset(): Promise<void> {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
console.log('🔃 Test data reset!');
}
}
// 🎮 Global test setup
export const testSetup = new IntegrationTestSetup();
💡 Explanation: We create a dedicated test environment with in-memory MongoDB to ensure tests are isolated and fast!
🎯 Jest Configuration
Configure Jest for integration testing:
// 📄 jest.integration.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// 🎯 Integration test pattern
testMatch: ['**/__tests__/integration/**/*.test.ts'],
// ⚡ Setup and teardown
globalSetup: '<rootDir>/test/setup/global-setup.ts',
globalTeardown: '<rootDir>/test/setup/global-teardown.ts',
setupFilesAfterEnv: ['<rootDir>/test/setup/integration-setup.ts'],
// 🕐 Longer timeout for integration tests
testTimeout: 30000,
// 📊 Coverage settings
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/types/**/*'
],
// 🏷️ Test reporting
reporters: [
'default',
['jest-html-reporters', {
publicPath: './test-reports',
filename: 'integration-report.html',
pageTitle: 'Integration Test Report 🔗'
}]
]
};
💡 Practical Integration Test Examples
🛒 Example 1: E-commerce Order Flow
Let’s test a complete order processing workflow:
// 🏗️ src/services/order-service.ts
import { UserService } from './user-service';
import { InventoryService } from './inventory-service';
import { PaymentService } from './payment-service';
import { EmailService } from './email-service';
export interface OrderItem {
productId: string;
quantity: number;
price: number;
emoji: string; // Every product needs personality!
}
export interface Order {
id: string;
userId: string;
items: OrderItem[];
total: number;
status: 'pending' | 'paid' | 'shipped' | 'delivered';
createdAt: Date;
}
export class OrderService {
constructor(
private userService: UserService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
private emailService: EmailService
) {}
// 🛍️ Process complete order
async processOrder(userId: string, items: OrderItem[]): Promise<Order> {
// 👤 Verify user exists
const user = await this.userService.findById(userId);
if (!user) {
throw new Error('👤 User not found!');
}
// 📦 Check inventory
for (const item of items) {
const available = await this.inventoryService.checkStock(item.productId);
if (available < item.quantity) {
throw new Error(`📦 Insufficient stock for ${item.emoji} ${item.productId}`);
}
}
// 💰 Calculate total
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
// 💳 Process payment
const paymentResult = await this.paymentService.charge(user.id, total);
if (!paymentResult.success) {
throw new Error('💳 Payment failed!');
}
// 📝 Create order
const order: Order = {
id: `order_${Date.now()}`,
userId,
items,
total,
status: 'paid',
createdAt: new Date()
};
// 📦 Reserve inventory
for (const item of items) {
await this.inventoryService.reserveStock(item.productId, item.quantity);
}
// 📧 Send confirmation email
await this.emailService.sendOrderConfirmation(user.email, order);
console.log(`🎉 Order ${order.id} processed successfully!`);
return order;
}
}
🧪 Integration Test:
// 🧪 __tests__/integration/order-flow.test.ts
import { testSetup } from '../setup/integration-setup';
import { OrderService } from '../../src/services/order-service';
import { UserService } from '../../src/services/user-service';
import { InventoryService } from '../../src/services/inventory-service';
import { PaymentService } from '../../src/services/payment-service';
import { EmailService } from '../../src/services/email-service';
describe('🛒 Order Processing Integration', () => {
let orderService: OrderService;
let userService: UserService;
let inventoryService: InventoryService;
let paymentService: PaymentService;
let emailService: EmailService;
beforeAll(async () => {
await testSetup.setup();
// 🏗️ Initialize services with real dependencies
userService = new UserService();
inventoryService = new InventoryService();
paymentService = new PaymentService();
emailService = new EmailService();
orderService = new OrderService(
userService,
inventoryService,
paymentService,
emailService
);
});
afterAll(async () => {
await testSetup.cleanup();
});
beforeEach(async () => {
await testSetup.reset();
});
describe('🎯 Successful Order Flow', () => {
it('should process complete order with all integrations', async () => {
// 📋 Arrange - Setup test data
const user = await userService.create({
id: 'user_123',
email: '[email protected]',
name: 'Happy Customer 😊'
});
// 📦 Add inventory
await inventoryService.addStock('prod_coffee', 100);
await inventoryService.addStock('prod_donut', 50);
const orderItems = [
{ productId: 'prod_coffee', quantity: 2, price: 4.99, emoji: '☕' },
{ productId: 'prod_donut', quantity: 1, price: 2.49, emoji: '🍩' }
];
// 🎬 Act - Process the order
const order = await orderService.processOrder(user.id, orderItems);
// ✅ Assert - Verify complete integration
expect(order).toBeDefined();
expect(order.userId).toBe(user.id);
expect(order.status).toBe('paid');
expect(order.total).toBe(12.47); // 2*4.99 + 1*2.49
// 🔍 Verify inventory was reserved
const coffeeStock = await inventoryService.checkStock('prod_coffee');
const donutStock = await inventoryService.checkStock('prod_donut');
expect(coffeeStock).toBe(98); // 100 - 2
expect(donutStock).toBe(49); // 50 - 1
// 📧 Verify email was sent (mock verification)
expect(emailService.sendOrderConfirmation).toHaveBeenCalledWith(
user.email,
expect.objectContaining({
id: order.id,
total: 12.47
})
);
console.log('🎉 Complete order integration test passed!');
});
it('should handle insufficient inventory gracefully', async () => {
// 📋 Setup user but limited inventory
const user = await userService.create({
id: 'user_456',
email: '[email protected]',
name: 'Disappointed Customer 😞'
});
// 📦 Only 1 coffee available
await inventoryService.addStock('prod_coffee', 1);
const orderItems = [
{ productId: 'prod_coffee', quantity: 5, price: 4.99, emoji: '☕' }
];
// 🎬 Attempt to order more than available
await expect(orderService.processOrder(user.id, orderItems))
.rejects
.toThrow('📦 Insufficient stock for ☕ prod_coffee');
// ✅ Verify no payment was processed
expect(paymentService.charge).not.toHaveBeenCalled();
console.log('✅ Inventory validation integration test passed!');
});
});
});
🎮 Example 2: User Authentication Flow
Testing authentication with multiple services:
// 🧪 __tests__/integration/auth-flow.test.ts
describe('🔐 Authentication Integration', () => {
let authService: AuthService;
let userService: UserService;
let tokenService: TokenService;
let emailService: EmailService;
beforeEach(async () => {
await testSetup.reset();
// 🏗️ Wire up services
tokenService = new TokenService();
emailService = new EmailService();
userService = new UserService();
authService = new AuthService(userService, tokenService, emailService);
});
it('should handle complete registration flow', async () => {
// 📋 Registration data
const registrationData = {
email: '[email protected]',
password: 'SecurePass123!',
name: 'New User 👋'
};
// 🎬 Register user
const result = await authService.register(registrationData);
// ✅ Verify user creation
expect(result.success).toBe(true);
expect(result.user.email).toBe(registrationData.email);
// 🔍 Verify user exists in database
const savedUser = await userService.findByEmail(registrationData.email);
expect(savedUser).toBeDefined();
expect(savedUser!.isEmailVerified).toBe(false);
// 📧 Verify verification email sent
expect(emailService.sendVerificationEmail).toHaveBeenCalledWith(
registrationData.email,
expect.any(String) // verification token
);
// 🎯 Test email verification
const verificationToken = (emailService.sendVerificationEmail as jest.Mock)
.mock.calls[0][1];
const verifyResult = await authService.verifyEmail(verificationToken);
expect(verifyResult.success).toBe(true);
// ✅ Verify user status updated
const verifiedUser = await userService.findByEmail(registrationData.email);
expect(verifiedUser!.isEmailVerified).toBe(true);
console.log('🎉 Complete auth flow integration test passed!');
});
it('should handle login with session management', async () => {
// 📋 Setup verified user
const user = await userService.create({
email: '[email protected]',
password: await authService.hashPassword('TestPass123!'),
name: 'Existing User 👤',
isEmailVerified: true
});
// 🎬 Login
const loginResult = await authService.login({
email: '[email protected]',
password: 'TestPass123!'
});
// ✅ Verify login success
expect(loginResult.success).toBe(true);
expect(loginResult.token).toBeDefined();
expect(loginResult.user.id).toBe(user.id);
// 🔍 Verify token can be validated
const tokenValidation = await tokenService.validateToken(loginResult.token);
expect(tokenValidation.valid).toBe(true);
expect(tokenValidation.userId).toBe(user.id);
console.log('🎉 Login integration test passed!');
});
});
🚀 Advanced Integration Testing Patterns
🧙♂️ Testing with External APIs
Handle external service dependencies:
// 🌐 Integration with external payment API
import nock from 'nock'; // HTTP mocking library
describe('💳 Payment Integration', () => {
beforeEach(() => {
// 🎭 Mock external payment API
nock('https://api.stripe.com')
.post('/v1/charges')
.reply(200, {
id: 'ch_test_123',
status: 'succeeded',
amount: 1999,
emoji: '💳'
});
});
afterEach(() => {
nock.cleanAll();
});
it('should process payment through external API', async () => {
const paymentService = new PaymentService();
const result = await paymentService.processPayment({
amount: 19.99,
currency: 'USD',
source: 'tok_visa',
description: 'Test purchase 🛍️'
});
expect(result.success).toBe(true);
expect(result.transactionId).toBe('ch_test_123');
console.log('💳 External API integration test passed!');
});
});
🏗️ Database Transaction Testing
Test complex database operations:
// 🗄️ Testing database transactions
describe('📊 Database Transaction Integration', () => {
it('should handle complex multi-table operations', async () => {
const session = await mongoose.startSession();
try {
await session.withTransaction(async () => {
// 👤 Create user within transaction
const user = await userService.create(
{ name: 'Transaction User 🔄', email: '[email protected]' },
{ session }
);
// 📝 Create profile
const profile = await profileService.create(
{ userId: user.id, bio: 'Test bio with transaction' },
{ session }
);
// 🎯 Create preferences
await preferenceService.create(
{ userId: user.id, theme: 'dark', notifications: true },
{ session }
);
return { user, profile };
});
// ✅ Verify all data was created
const user = await userService.findByEmail('[email protected]');
const profile = await profileService.findByUserId(user!.id);
const preferences = await preferenceService.findByUserId(user!.id);
expect(user).toBeDefined();
expect(profile).toBeDefined();
expect(preferences).toBeDefined();
console.log('🔄 Transaction integration test passed!');
} finally {
await session.endSession();
}
});
});
⚠️ Common Integration Testing Pitfalls
😱 Pitfall 1: Test Data Contamination
// ❌ Wrong - tests affect each other!
describe('🚫 Bad Integration Tests', () => {
const sharedUser = { id: 'shared_user', name: 'Shared User' };
it('test 1 modifies shared data', async () => {
await userService.create(sharedUser);
// Test modifies sharedUser
});
it('test 2 assumes clean state', async () => {
// 💥 Fails because test 1 contaminated data!
const user = await userService.findById('shared_user');
expect(user).toBeNull(); // This will fail!
});
});
// ✅ Correct - isolated test data!
describe('✅ Good Integration Tests', () => {
beforeEach(async () => {
await testSetup.reset(); // Clean slate for each test
});
it('test 1 uses fresh data', async () => {
const user = await userService.create({
id: 'test1_user',
name: 'Test 1 User 🔷'
});
// Test runs with clean data
});
it('test 2 uses fresh data', async () => {
const user = await userService.create({
id: 'test2_user',
name: 'Test 2 User 🔶'
});
// Test runs with clean data
});
});
🤯 Pitfall 2: Over-Mocking Dependencies
// ❌ Wrong - mocking everything defeats integration purpose!
describe('🚫 Over-Mocked Integration', () => {
it('mocks all dependencies', async () => {
// This is just a unit test in disguise!
jest.mocked(userService.findById).mockResolvedValue(mockUser);
jest.mocked(paymentService.charge).mockResolvedValue(mockPayment);
jest.mocked(emailService.send).mockResolvedValue(mockEmail);
// 💥 Not testing real integrations!
const result = await orderService.processOrder('user1', []);
});
});
// ✅ Correct - mock only external dependencies!
describe('✅ Proper Integration Testing', () => {
it('tests real service interactions', async () => {
// Only mock external services
nock('https://external-payment-api.com')
.post('/charge')
.reply(200, { success: true });
// Let internal services interact for real
const user = await userService.create(testUser);
const result = await orderService.processOrder(user.id, orderItems);
// ✅ Tests real internal integrations!
});
});
🛠️ Integration Testing Best Practices
- 🎯 Test Real Interactions: Don’t mock internal service communication
- 🧹 Clean Test Data: Reset database state between tests
- ⚡ Use Test Doubles: Mock only external dependencies
- 🔧 Environment Isolation: Use separate test databases
- 📊 Monitor Performance: Integration tests can be slower
- 🎭 Test Error Cases: Verify failure scenarios work correctly
- 🔍 Meaningful Assertions: Test the integration, not implementation
🧪 Hands-On Exercise
🎯 Challenge: Build a Blog System Integration Test
Create integration tests for a blogging platform:
📋 Requirements:
- ✅ Test user registration and login flow
- 📝 Test blog post creation with author relationship
- 💬 Test commenting system with notifications
- 🏷️ Test tagging and search functionality
- 📧 Test email notifications for new comments
- 🔒 Test authorization (only authors can edit posts)
🚀 Bonus Points:
- Add image upload integration
- Test pagination and filtering
- Implement rate limiting tests
- Add analytics tracking integration
💡 Solution Starter
🔍 Click to see solution starter
// 🎯 Blog integration test starter
describe('📝 Blog System Integration', () => {
let blogService: BlogService;
let userService: UserService;
let commentService: CommentService;
let notificationService: NotificationService;
beforeEach(async () => {
await testSetup.reset();
// 🏗️ Initialize services
userService = new UserService();
notificationService = new NotificationService();
commentService = new CommentService(notificationService);
blogService = new BlogService(userService, commentService);
});
it('should handle complete blog workflow', async () => {
// 👤 Create author
const author = await userService.create({
name: 'Blog Author ✍️',
email: '[email protected]'
});
// 📝 Create blog post
const post = await blogService.createPost({
title: 'My Amazing Post 🚀',
content: 'This is awesome content!',
authorId: author.id,
tags: ['typescript', 'testing']
});
// 👤 Create commenter
const commenter = await userService.create({
name: 'Happy Reader 😊',
email: '[email protected]'
});
// 💬 Add comment
const comment = await commentService.addComment({
postId: post.id,
authorId: commenter.id,
content: 'Great post! 👏'
});
// ✅ Verify integration
expect(post.authorId).toBe(author.id);
expect(comment.postId).toBe(post.id);
// 📧 Verify notification sent to post author
expect(notificationService.sendCommentNotification)
.toHaveBeenCalledWith(author.email, expect.any(Object));
console.log('🎉 Blog integration test passed!');
});
});
🎓 Key Takeaways
You’ve mastered integration testing! Here’s what you can now do:
- ✅ Write comprehensive integration tests that verify component interactions 🔗
- ✅ Set up isolated test environments with proper cleanup 🧹
- ✅ Test complex workflows like authentication and order processing 🛒
- ✅ Handle external dependencies using mocks and stubs 🎭
- ✅ Debug integration issues and avoid common pitfalls 🐛
- ✅ Apply best practices for maintainable integration tests 🚀
Remember: Integration tests are your safety net for ensuring components work together harmoniously! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered integration testing in TypeScript!
Here’s what to explore next:
- 💻 Practice with the blog system exercise above
- 🏗️ Add integration tests to your current project
- 📚 Learn about End-to-End testing with Cypress (Tutorial #152)
- 🌟 Explore contract testing for microservices
- 🔍 Set up continuous integration for your tests
Remember: Integration tests give you confidence that your system works as a whole. Keep testing, keep integrating, and build rock-solid applications! 🚀
Happy testing! 🎉🔗✨