Integration Testing
Test how components work together: API integration, database testing, and component interactions
What is Integration Testing?
Integration testing verifies that different modules, services, or components of your application work correctly together. Unlike unit tests that isolate individual functions, integration tests check the interactions between multiple parts of your system.
🎯 Integration Test Goals:
Catch bugs that occur when components interact, verify API contracts, test database operations, and ensure proper data flow between modules.
Types of Integration Tests
🔌 API Integration
Testing frontend-backend communication
- • HTTP requests and responses
- • Authentication flows
- • Data serialization
- • Error handling
🗄️ Database Integration
Testing database operations
- • CRUD operations
- • Transactions
- • Relationships and joins
- • Migrations
🧩 Component Integration
Testing React component trees
- • Parent-child communication
- • Context and state management
- • Event propagation
- • Form submissions
🔗 Service Integration
Testing service layer interactions
- • Business logic workflows
- • External API calls
- • Message queues
- • Cache interactions
Testing API Integration
// api.js - API client
export class API {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
const response = await fetch(this.baseURL + endpoint);
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(this.baseURL + endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
return response.json();
}
}
// api.integration.test.js - Integration test with real HTTP
import { API } from './api';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
// Mock Service Worker - intercepts real HTTP requests
const server = setupServer(
rest.get('https://api.example.com/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
])
);
}),
rest.post('https://api.example.com/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.json({
id: 3,
...body,
})
);
}),
rest.get('https://api.example.com/error', (req, res, ctx) => {
return res(ctx.status(500));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('API Integration', () => {
test('fetches users successfully', async () => {
const api = new API('https://api.example.com');
const users = await api.get('/users');
expect(users).toHaveLength(2);
expect(users[0].name).toBe('Alice');
});
test('creates user successfully', async () => {
const api = new API('https://api.example.com');
const newUser = await api.post('/users', {
name: 'Charlie',
email: 'charlie@example.com',
});
expect(newUser.id).toBe(3);
expect(newUser.name).toBe('Charlie');
});
test('handles server errors', async () => {
const api = new API('https://api.example.com');
await expect(api.get('/error')).rejects.toThrow('Request failed: 500');
});
});
Testing React Component Integration
// TodoList.jsx - Parent component
import { useState } from 'react';
import TodoForm from './TodoForm';
import TodoItem from './TodoItem';
export function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
My Todos
{todos.map(todo => (
))}
{todos.length === 0 && No todos yet
}
);
}
// TodoForm.jsx - Child component
export function TodoForm({ onAdd }) {
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
onAdd(input);
setInput('');
}
};
return (
);
}
// TodoItem.jsx - Child component
export function TodoItem({ todo, onToggle, onDelete }) {
return (
onToggle(todo.id)}
/>
{todo.text}
);
}
// TodoList.integration.test.jsx - Component integration test
import { render, screen, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';
describe('TodoList Integration', () => {
test('user can add, toggle, and delete todos', async () => {
const user = userEvent.setup();
render( );
// Initially empty
expect(screen.getByText('No todos yet')).toBeInTheDocument();
// Add first todo
const input = screen.getByPlaceholderText('Add a todo...');
await user.type(input, 'Buy milk');
await user.click(screen.getByText('Add'));
// Verify todo was added
expect(screen.getByText('Buy milk')).toBeInTheDocument();
expect(screen.queryByText('No todos yet')).not.toBeInTheDocument();
expect(input).toHaveValue(''); // Input cleared
// Add second todo
await user.type(input, 'Walk dog');
await user.click(screen.getByText('Add'));
expect(screen.getByText('Walk dog')).toBeInTheDocument();
// Toggle first todo
const firstCheckbox = screen.getAllByRole('checkbox')[0];
await user.click(firstCheckbox);
const buyMilk = screen.getByText('Buy milk');
expect(buyMilk).toHaveStyle({ textDecoration: 'line-through' });
// Delete second todo
const deleteButtons = screen.getAllByText('Delete');
await user.click(deleteButtons[1]);
expect(screen.queryByText('Walk dog')).not.toBeInTheDocument();
expect(screen.getByText('Buy milk')).toBeInTheDocument();
});
test('does not add empty todos', async () => {
const user = userEvent.setup();
render( );
const addButton = screen.getByText('Add');
// Try to add empty
await user.click(addButton);
expect(screen.getByText('No todos yet')).toBeInTheDocument();
// Try to add whitespace
await user.type(screen.getByPlaceholderText('Add a todo...'), ' ');
await user.click(addButton);
expect(screen.getByText('No todos yet')).toBeInTheDocument();
});
test('maintains todo state across multiple operations', async () => {
const user = userEvent.setup();
render( );
// Add three todos
const input = screen.getByPlaceholderText('Add a todo...');
await user.type(input, 'First');
await user.click(screen.getByText('Add'));
await user.type(input, 'Second');
await user.click(screen.getByText('Add'));
await user.type(input, 'Third');
await user.click(screen.getByText('Add'));
// Toggle first and third
const checkboxes = screen.getAllByRole('checkbox');
await user.click(checkboxes[0]);
await user.click(checkboxes[2]);
// Delete second
const deleteButtons = screen.getAllByText('Delete');
await user.click(deleteButtons[1]);
// Verify final state
expect(screen.getByText('First')).toHaveStyle({
textDecoration: 'line-through'
});
expect(screen.queryByText('Second')).not.toBeInTheDocument();
expect(screen.getByText('Third')).toHaveStyle({
textDecoration: 'line-through'
});
});
});
Testing with Context and State Management
// AuthContext.jsx
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username, password) => {
// Simplified login
if (password === 'correct') {
setUser({ username });
return true;
}
return false;
};
const logout = () => {
setUser(null);
};
return (
{children}
);
}
export const useAuth = () => useContext(AuthContext);
// Protected.jsx - Component using context
export function Protected() {
const { user, logout } = useAuth();
if (!user) {
return Please log in;
}
return (
Welcome {user.username}!
);
}
// LoginForm.jsx
export function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const handleSubmit = (e) => {
e.preventDefault();
const success = login(username, password);
if (!success) {
setError('Invalid credentials');
}
};
return (
);
}
// Auth.integration.test.jsx - Testing with context
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AuthProvider } from './AuthContext';
import { LoginForm } from './LoginForm';
import { Protected } from './Protected';
function App() {
return (
);
}
describe('Authentication Integration', () => {
test('user can log in and see protected content', async () => {
const user = userEvent.setup();
render( );
// Initially not logged in
expect(screen.getByText('Please log in')).toBeInTheDocument();
// Fill login form
await user.type(screen.getByPlaceholderText('Username'), 'alice');
await user.type(screen.getByPlaceholderText('Password'), 'correct');
await user.click(screen.getByText('Login'));
// Verify logged in
await waitFor(() => {
expect(screen.getByText('Welcome alice!')).toBeInTheDocument();
});
expect(screen.queryByText('Please log in')).not.toBeInTheDocument();
});
test('shows error on invalid login', async () => {
const user = userEvent.setup();
render( );
await user.type(screen.getByPlaceholderText('Username'), 'alice');
await user.type(screen.getByPlaceholderText('Password'), 'wrong');
await user.click(screen.getByText('Login'));
expect(await screen.findByRole('alert')).toHaveTextContent(
'Invalid credentials'
);
expect(screen.getByText('Please log in')).toBeInTheDocument();
});
test('user can log out', async () => {
const user = userEvent.setup();
render( );
// Log in first
await user.type(screen.getByPlaceholderText('Username'), 'alice');
await user.type(screen.getByPlaceholderText('Password'), 'correct');
await user.click(screen.getByText('Login'));
await waitFor(() => {
expect(screen.getByText('Welcome alice!')).toBeInTheDocument();
});
// Log out
await user.click(screen.getByText('Logout'));
expect(screen.getByText('Please log in')).toBeInTheDocument();
});
});
Database Integration Testing
// userRepository.js - Database layer
export class UserRepository {
constructor(db) {
this.db = db;
}
async create(user) {
const result = await this.db.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[user.name, user.email]
);
return result.rows[0];
}
async findById(id) {
const result = await this.db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async findByEmail(email) {
const result = await this.db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
return result.rows[0] || null;
}
async update(id, updates) {
const result = await this.db.query(
'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
[updates.name, updates.email, id]
);
return result.rows[0] || null;
}
async delete(id) {
await this.db.query('DELETE FROM users WHERE id = $1', [id]);
}
}
// userRepository.integration.test.js - Database integration test
import { UserRepository } from './userRepository';
import { setupTestDatabase, cleanupTestDatabase } from './test-utils';
describe('UserRepository Integration', () => {
let db;
let repo;
beforeAll(async () => {
// Create test database connection
db = await setupTestDatabase();
repo = new UserRepository(db);
});
afterAll(async () => {
await cleanupTestDatabase(db);
});
beforeEach(async () => {
// Clear database before each test
await db.query('DELETE FROM users');
});
test('creates and retrieves user', async () => {
const user = await repo.create({
name: 'Alice',
email: 'alice@example.com',
});
expect(user.id).toBeDefined();
expect(user.name).toBe('Alice');
expect(user.email).toBe('alice@example.com');
const found = await repo.findById(user.id);
expect(found).toEqual(user);
});
test('finds user by email', async () => {
await repo.create({
name: 'Bob',
email: 'bob@example.com',
});
const found = await repo.findByEmail('bob@example.com');
expect(found.name).toBe('Bob');
});
test('returns null for non-existent user', async () => {
const found = await repo.findById(99999);
expect(found).toBeNull();
});
test('updates user', async () => {
const user = await repo.create({
name: 'Charlie',
email: 'charlie@example.com',
});
const updated = await repo.update(user.id, {
name: 'Charles',
email: 'charles@example.com',
});
expect(updated.name).toBe('Charles');
expect(updated.email).toBe('charles@example.com');
const found = await repo.findById(user.id);
expect(found.name).toBe('Charles');
});
test('deletes user', async () => {
const user = await repo.create({
name: 'David',
email: 'david@example.com',
});
await repo.delete(user.id);
const found = await repo.findById(user.id);
expect(found).toBeNull();
});
test('handles duplicate email constraint', async () => {
await repo.create({
name: 'Eve',
email: 'eve@example.com',
});
await expect(
repo.create({
name: 'Evil Eve',
email: 'eve@example.com',
})
).rejects.toThrow();
});
});
⚠️ Integration Testing Challenges
- Slower than unit tests: May involve real databases, APIs, or file system
- More setup required: Need to configure test environments, seed data
- Can be flaky: Network issues, timing problems, race conditions
- Harder to debug: More moving parts mean more places bugs can hide
- Environment dependencies: May need Docker, test databases, etc.
💡 Integration Testing Best Practices
- ✓ Use test databases or in-memory databases for DB tests
- ✓ Clean up data between tests to ensure independence
- ✓ Mock external services you don't control (third-party APIs)
- ✓ Test the integration points, not implementation details
- ✓ Keep integration tests focused on critical paths
- ✓ Use tools like MSW for API mocking
- ✓ Run integration tests in CI/CD pipeline
- ✓ Balance speed with realism - don't mock everything
📚 More Testing Topics
Explore all 6 testing topics to build a comprehensive understanding of software testing.
View All Topics