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 (
setInput(e.target.value)} placeholder="Add a todo..." />
); } // 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 (
setUsername(e.target.value)} /> setPassword(e.target.value)} /> {error &&
{error}
}
); }
// 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