Integration Testing JavaScript Applications

Ensuring that multiple components of your application work together correctly

What is Integration Testing?

Integration testing verifies that different parts of your application work together correctly. While unit tests focus on isolated components, integration tests examine the interactions between multiple components, providing confidence that your application functions as a whole.

flowchart TD A[Unit Testing] --> B[Test Individual Components in Isolation] C[Integration Testing] --> D[Test How Components Work Together] E[End-to-End Testing] --> F[Test Complete User Flows] B -.-> D D -.-> F style A fill:#e1f5fe style C fill:#ffe0b2 style E fill:#f9a825

Real-world analogy: Think of a car manufacturing process. Unit tests verify that individual parts (engine, brakes, transmission) work correctly in isolation. Integration tests check that these parts work together properly—for example, that the engine connects correctly to the transmission. End-to-end tests verify the complete car functions as expected on the road.

Why Integration Testing Matters

pie title "Test Types Distribution" "Unit Tests" : 70 "Integration Tests" : 20 "End-to-End Tests" : 10

Real-world example: Spotify's engineering team discovered that while their microservices passed all unit tests individually, integration tests revealed issues when they interacted. For instance, one service expected dates in ISO format, while another sent them in Unix timestamp format. Unit tests didn't catch this mismatch, but integration tests immediately highlighted the issue.

Integration Testing Approaches

Top-Down Approach

Start with high-level components and gradually test lower-level components.

Bottom-Up Approach

Start with lower-level components and gradually test higher-level components.

Sandwich Approach (Hybrid)

Combine both approaches, testing both high-level and low-level components simultaneously.

graph TD subgraph "Top-Down" A1[User Interface] --> B1[Business Logic] B1 --> C1[Data Access] end subgraph "Bottom-Up" A2[Data Access] --> B2[Business Logic] B2 --> C2[User Interface] end subgraph "Sandwich" A3[User Interface] --> B3[Business Logic] C3[Data Access] --> B3 end

What to Test in Integration Tests

Focus Areas for Integration Testing

mindmap root((Integration Test Focus)) Data Flow Component to Component Parent to Child Through Context/Redux User Interactions Multi-step Workflows Form Submission Navigation Flow External Services API Responses Error Scenarios Authentication Cross-cutting Concerns Error Handling Loading States Performance

Tools for JavaScript Integration Testing

Primary Testing Tools

Supporting Tools

Setting Up Integration Tests with Jest

Project Structure

It's helpful to separate integration tests from unit tests:

my-app/
├── src/
│   ├── components/
│   ├── pages/
│   ├── services/
│   └── ...
├── tests/
│   ├── unit/
│   └── integration/
└── jest.config.js

Jest Configuration

// jest.config.js
module.exports = {
  // Common configuration
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['./jest.setup.js'],
  
  // Projects for different test types
  projects: [
    {
      displayName: 'unit',
      testMatch: ['/tests/unit/**/*.test.js'],
      // Unit test specific settings
    },
    {
      displayName: 'integration',
      testMatch: ['/tests/integration/**/*.test.js'],
      // May have longer timeouts for integration tests
      testTimeout: 10000,
    }
  ]
};

// jest.setup.js
import '@testing-library/jest-dom';
// Other global setup

Running Integration Tests

// Run only integration tests
npm test -- --selectProjects=integration

// Run specific integration test file
npm test -- tests/integration/checkout-flow.test.js

Frontend Integration Testing Example

Let's test a user authentication flow with multiple components:

flowchart TD A[LoginForm] --> B[AuthService] B --> C[UserContext] C --> D[ProtectedRoute] D --> E[Dashboard]

Components we'll integrate:

// LoginForm.jsx
function LoginForm({ onLogin }) {
  const [credentials, setCredentials] = useState({ email: '', password: '' });
  const [error, setError] = useState('');
  
  const handleChange = (e) => {
    setCredentials({
      ...credentials,
      [e.target.name]: e.target.value
    });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    
    try {
      await onLogin(credentials);
    } catch (err) {
      setError(err.message);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {error && <div role="alert">{error}</div>}
      
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={credentials.email}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          name="password"
          type="password"
          value={credentials.password}
          onChange={handleChange}
          required
        />
      </div>
      
      <button type="submit">Log In</button>
    </form>
  );
}
// authService.js
export async function login(credentials) {
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credentials)
  });
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Login failed');
  }
  
  const data = await response.json();
  localStorage.setItem('token', data.token);
  return data.user;
}

export function logout() {
  localStorage.removeItem('token');
}

export async function getCurrentUser() {
  const token = localStorage.getItem('token');
  
  if (!token) {
    return null;
  }
  
  const response = await fetch('/api/me', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  
  if (!response.ok) {
    localStorage.removeItem('token');
    return null;
  }
  
  const data = await response.json();
  return data.user;
}
// UserContext.jsx
import React, { createContext, useState, useEffect, useContext } from 'react';
import { getCurrentUser, login, logout } from './authService';

const UserContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function loadUser() {
      try {
        const currentUser = await getCurrentUser();
        setUser(currentUser);
      } catch (error) {
        console.error('Failed to load user:', error);
      } finally {
        setLoading(false);
      }
    }
    
    loadUser();
  }, []);
  
  const handleLogin = async (credentials) => {
    const loggedInUser = await login(credentials);
    setUser(loggedInUser);
    return loggedInUser;
  };
  
  const handleLogout = () => {
    logout();
    setUser(null);
  };
  
  const value = {
    user,
    loading,
    login: handleLogin,
    logout: handleLogout
  };
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

export function useUser() {
  return useContext(UserContext);
}
// ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useUser } from './UserContext';

function ProtectedRoute({ children }) {
  const { user, loading } = useUser();
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
}
// LoginPage.jsx
import { useNavigate } from 'react-router-dom';
import { useUser } from './UserContext';
import LoginForm from './LoginForm';

function LoginPage() {
  const { login } = useUser();
  const navigate = useNavigate();
  
  const handleLogin = async (credentials) => {
    await login(credentials);
    navigate('/dashboard');
  };
  
  return (
    <div>
      <h1>Login</h1>
      <LoginForm onLogin={handleLogin} />
    </div>
  );
}

Integration Test for the Login Flow

// login-flow.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

import { UserProvider } from './UserContext';
import LoginPage from './LoginPage';
import Dashboard from './Dashboard';
import ProtectedRoute from './ProtectedRoute';

// Mock API with MSW
const server = setupServer(
  // Mock login endpoint
  rest.post('/api/login', (req, res, ctx) => {
    const { email, password } = req.body;
    
    if (email === 'user@example.com' && password === 'password123') {
      return res(
        ctx.json({
          token: 'fake-token-123',
          user: {
            id: '123',
            name: 'Test User',
            email: 'user@example.com',
            role: 'user'
          }
        })
      );
    }
    
    return res(
      ctx.status(401),
      ctx.json({ message: 'Invalid credentials' })
    );
  }),
  
  // Mock current user endpoint
  rest.get('/api/me', (req, res, ctx) => {
    const authHeader = req.headers.get('Authorization');
    
    if (authHeader === 'Bearer fake-token-123') {
      return res(
        ctx.json({
          user: {
            id: '123',
            name: 'Test User',
            email: 'user@example.com',
            role: 'user'
          }
        })
      );
    }
    
    return res(
      ctx.status(401),
      ctx.json({ message: 'Unauthorized' })
    );
  })
);

// Setup and teardown MSW
beforeAll(() => server.listen());
afterEach(() => {
  server.resetHandlers();
  window.localStorage.clear();
});
afterAll(() => server.close());

// Helper to render the app with routing and context
function renderApp(initialRoute = '/login') {
  return render(
    <MemoryRouter initialEntries={[initialRoute]}>
      <UserProvider>
        <Routes>
          <Route path="/login" element={<LoginPage />} />
          <Route 
            path="/dashboard" 
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            } 
          />
        </Routes>
      </UserProvider>
    </MemoryRouter>
  );
}

describe('Login Flow', () => {
  test('redirects to dashboard after successful login', async () => {
    const user = userEvent.setup();
    renderApp();
    
    // Assert that we're on the login page
    expect(screen.getByRole('heading', { name: /login/i })).toBeInTheDocument();
    
    // Fill in the login form
    await user.type(screen.getByLabelText(/email/i), 'user@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    
    // Submit the form
    await user.click(screen.getByRole('button', { name: /log in/i }));
    
    // Wait for redirection to dashboard
    await waitFor(() => {
      expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument();
    });
    
    // Check that user data is displayed on dashboard
    expect(screen.getByText(/welcome, test user/i)).toBeInTheDocument();
  });
  
  test('shows error message with invalid credentials', async () => {
    const user = userEvent.setup();
    renderApp();
    
    // Fill in the login form with wrong password
    await user.type(screen.getByLabelText(/email/i), 'user@example.com');
    await user.type(screen.getByLabelText(/password/i), 'wrongpassword');
    
    // Submit the form
    await user.click(screen.getByRole('button', { name: /log in/i }));
    
    // Check that error message is displayed
    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/invalid credentials/i);
    });
    
    // Verify we're still on the login page
    expect(screen.getByRole('heading', { name: /login/i })).toBeInTheDocument();
  });
  
  test('redirects to login when accessing protected route without authentication', async () => {
    renderApp('/dashboard');
    
    // First, should show loading state
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    
    // Then, should redirect to login page
    await waitFor(() => {
      expect(screen.getByRole('heading', { name: /login/i })).toBeInTheDocument();
    });
  });
  
  test('stays on dashboard if user is already logged in', async () => {
    // Pre-set token in localStorage
    localStorage.setItem('token', 'fake-token-123');
    
    renderApp('/dashboard');
    
    // First, should show loading state
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    
    // Then, should show dashboard
    await waitFor(() => {
      expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument();
    });
  });
});

Backend Integration Testing Example

Let's test a backend workflow integrating controllers, services, and database access:

flowchart LR A[API Controller] --> B[Service Layer] B --> C[Repository Layer] C --> D[Database]

First, the components we'll test:

// userRepository.js
const db = require('./database');

async function findByEmail(email) {
  return db.query('SELECT * FROM users WHERE email = ?', [email]);
}

async function create(user) {
  const result = await db.query(
    'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)',
    [user.name, user.email, user.passwordHash]
  );
  
  return { id: result.insertId, ...user };
}

module.exports = {
  findByEmail,
  create
};
// userService.js
const bcrypt = require('bcrypt');
const userRepository = require('./userRepository');

async function registerUser({ name, email, password }) {
  // Check if user already exists
  const existingUser = await userRepository.findByEmail(email);
  
  if (existingUser.length > 0) {
    throw new Error('User with this email already exists');
  }
  
  // Hash password
  const passwordHash = await bcrypt.hash(password, 10);
  
  // Create user
  const user = await userRepository.create({
    name,
    email,
    passwordHash
  });
  
  // Remove sensitive data
  const { passwordHash: _, ...userWithoutPassword } = user;
  
  return userWithoutPassword;
}

module.exports = {
  registerUser
};
// userController.js
const userService = require('./userService');

async function register(req, res) {
  try {
    const { name, email, password } = req.body;
    
    // Validate input
    if (!name || !email || !password) {
      return res.status(400).json({ message: 'All fields are required' });
    }
    
    // Register user
    const user = await userService.registerUser({ name, email, password });
    
    res.status(201).json({ user });
  } catch (error) {
    if (error.message === 'User with this email already exists') {
      return res.status(409).json({ message: error.message });
    }
    
    res.status(500).json({ message: 'Failed to register user' });
  }
}

module.exports = {
  register
};
// routes.js
const express = require('express');
const userController = require('./userController');

const router = express.Router();

router.post('/users', userController.register);

module.exports = router;
// app.js
const express = require('express');
const routes = require('./routes');

const app = express();

app.use(express.json());
app.use('/api', routes);

module.exports = app;

Integration Test for User Registration

// user-registration.test.js
const request = require('supertest');
const app = require('./app');
const bcrypt = require('bcrypt');
const db = require('./database');

// Mock the database module
jest.mock('./database');

describe('User Registration API', () => {
  beforeEach(() => {
    // Clear mocks before each test
    jest.clearAllMocks();
  });
  
  test('successfully registers a user', async () => {
    // Mock database responses
    db.query.mockImplementation((query, params) => {
      if (query.includes('SELECT')) {
        // No existing user found
        return [];
      } else if (query.includes('INSERT')) {
        // User was created successfully
        return { insertId: 1 };
      }
    });
    
    // Mock bcrypt hash
    bcrypt.hash = jest.fn().mockResolvedValue('hashed_password');
    
    // Test data
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'password123'
    };
    
    // Make API request
    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect(201);
    
    // Verify response
    expect(response.body.user).toEqual(expect.objectContaining({
      id: 1,
      name: 'John Doe',
      email: 'john@example.com'
    }));
    
    // Password should not be in the response
    expect(response.body.user.password).toBeUndefined();
    expect(response.body.user.passwordHash).toBeUndefined();
    
    // Verify database was called correctly
    expect(db.query).toHaveBeenCalledTimes(2);
    expect(db.query).toHaveBeenNthCalledWith(
      1,
      expect.stringContaining('SELECT'),
      ['john@example.com']
    );
    expect(db.query).toHaveBeenNthCalledWith(
      2,
      expect.stringContaining('INSERT'),
      ['John Doe', 'john@example.com', 'hashed_password']
    );
    
    // Verify bcrypt was called correctly
    expect(bcrypt.hash).toHaveBeenCalledWith('password123', 10);
  });
  
  test('returns 409 when user with email already exists', async () => {
    // Mock database to return existing user
    db.query.mockImplementation((query) => {
      if (query.includes('SELECT')) {
        return [{ id: 1, email: 'john@example.com', name: 'Existing User' }];
      }
    });
    
    // Test data
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'password123'
    };
    
    // Make API request
    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect(409);
    
    // Verify response
    expect(response.body.message).toBe('User with this email already exists');
    
    // Verify only the find query was executed, not the insert
    expect(db.query).toHaveBeenCalledTimes(1);
  });
  
  test('returns 400 when required fields are missing', async () => {
    // Test with missing name
    const response1 = await request(app)
      .post('/api/users')
      .send({
        email: 'john@example.com',
        password: 'password123'
      })
      .expect(400);
    
    expect(response1.body.message).toBe('All fields are required');
    
    // Test with missing email
    const response2 = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        password: 'password123'
      })
      .expect(400);
    
    expect(response2.body.message).toBe('All fields are required');
    
    // Test with missing password
    const response3 = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        email: 'john@example.com'
      })
      .expect(400);
    
    expect(response3.body.message).toBe('All fields are required');
    
    // Verify the database was never queried
    expect(db.query).not.toHaveBeenCalled();
  });
});

Full-Stack Integration Testing

To test the complete stack, we can combine frontend and backend testing approaches:

// full-stack-integration.test.js
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
const request = require('supertest');
const { render, screen, waitFor } = require('@testing-library/react');
const userEvent = require('@testing-library/user-event');
const { BrowserRouter } = require('react-router-dom');

const server = require('./server');
const App = require('./App');

describe('Full-Stack Integration', () => {
  let mongoServer;
  
  // Setup in-memory MongoDB server
  beforeAll(async () => {
    mongoServer = await MongoMemoryServer.create();
    const uri = mongoServer.getUri();
    await mongoose.connect(uri);
  });
  
  // Clean up after tests
  afterAll(async () => {
    await mongoose.disconnect();
    await mongoServer.stop();
    server.close();
  });
  
  // Clean database between tests
  beforeEach(async () => {
    await mongoose.connection.db.dropDatabase();
  });
  
  test('user can register and then login', async () => {
    const user = userEvent.setup();
    
    // First, register a user through the API
    await request(server)
      .post('/api/users')
      .send({
        name: 'Test User',
        email: 'test@example.com',
        password: 'password123'
      })
      .expect(201);
    
    // Now render the React app
    render(
      <BrowserRouter>
        <App />
      </BrowserRouter>
    );
    
    // Navigate to login page if not already there
    const loginLink = screen.getByRole('link', { name: /login/i });
    await user.click(loginLink);
    
    // Fill in the login form
    await user.type(screen.getByLabelText(/email/i), 'test@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    
    // Submit the form
    await user.click(screen.getByRole('button', { name: /log in/i }));
    
    // Verify user is logged in and redirected to dashboard
    await waitFor(() => {
      expect(screen.getByText(/welcome, test user/i)).toBeInTheDocument();
    });
  });
});
sequenceDiagram participant Client participant API participant Database Client->>API: POST /api/users (Register) API->>Database: Check if user exists Database-->>API: User doesn't exist API->>Database: Create new user Database-->>API: User created API-->>Client: 201 Created Client->>API: POST /api/login (Login) API->>Database: Find user by email Database-->>API: User found API->>API: Verify password API-->>Client: 200 OK with token Client->>API: GET /api/me (Auth check) API->>API: Verify token API->>Database: Get user data Database-->>API: User data API-->>Client: 200 OK with user data

Advanced Integration Testing Techniques

Testing with Real Databases

While mocking the database works for many cases, sometimes you need to test with a real database:

// Using test containers for real database tests
const { GenericContainer } = require('testcontainers');

describe('Integration with real PostgreSQL', () => {
  let container;
  let db;
  
  beforeAll(async () => {
    // Start PostgreSQL container
    container = await new GenericContainer('postgres:13')
      .withExposedPorts(5432)
      .withEnv('POSTGRES_USER', 'test')
      .withEnv('POSTGRES_PASSWORD', 'test')
      .withEnv('POSTGRES_DB', 'testdb')
      .start();
    
    // Connect to database
    const connectionString = `postgresql://test:test@localhost:${container.getMappedPort(5432)}/testdb`;
    db = require('your-db-client')(connectionString);
    
    // Set up schema
    await db.query(`CREATE TABLE users (
      id SERIAL PRIMARY KEY, 
      name TEXT, 
      email TEXT UNIQUE, 
      password_hash TEXT
    )`);
  });
  
  afterAll(async () => {
    await db.end();
    await container.stop();
  });
  
  test('can create and find user', async () => {
    // Your test using real database
  });
});

Testing Microservices Integration

When testing microservices, you may need to orchestrate multiple services:

// Using docker-compose for microservices integration tests
const { DockerComposeEnvironment } = require('testcontainers');
const path = require('path');

describe('Microservices Integration', () => {
  let environment;
  let authApiUrl;
  let productApiUrl;
  
  beforeAll(async () => {
    // Start all services defined in docker-compose.yml
    environment = await new DockerComposeEnvironment(
      path.resolve(__dirname, '..'), 
      'docker-compose.test.yml'
    ).up();
    
    // Get service URLs
    const authService = environment.getContainer('auth-service');
    const productService = environment.getContainer('product-service');
    
    authApiUrl = `http://localhost:${authService.getMappedPort(3000)}`;
    productApiUrl = `http://localhost:${productService.getMappedPort(3001)}`;
  });
  
  afterAll(async () => {
    await environment.down();
  });
  
  test('authenticated user can access product API', async () => {
    // First login through auth service
    const loginResponse = await request(authApiUrl)
      .post('/api/login')
      .send({ email: 'test@example.com', password: 'password' });
    
    const { token } = loginResponse.body;
    
    // Then use token to access product API
    const productResponse = await request(productApiUrl)
      .get('/api/products')
      .set('Authorization', `Bearer ${token}`);
    
    expect(productResponse.status).toBe(200);
    expect(productResponse.body).toHaveProperty('products');
  });
});

Test Fixtures and Factories

Create consistent test data with factories:

// factories.js
const { faker } = require('@faker-js/faker');
const bcrypt = require('bcrypt');

function createUser(overrides = {}) {
  return {
    name: faker.person.fullName(),
    email: faker.internet.email(),
    passwordHash: bcrypt.hashSync('password123', 10),
    ...overrides
  };
}

function createProduct(overrides = {}) {
  return {
    name: faker.commerce.productName(),
    price: parseFloat(faker.commerce.price()),
    description: faker.commerce.productDescription(),
    ...overrides
  };
}

module.exports = {
  createUser,
  createProduct
};

// Usage in tests
const { createUser } = require('./factories');

test('user can update profile', async () => {
  const user = createUser();
  // Insert user into test database
  
  // Test profile update logic
});

Best Practices for Integration Testing

General Best Practices

Performance Considerations

Avoiding Common Pitfalls

Real-world example: Netflix's engineering team discovered that their integration tests were failing inconsistently in their CI environment. They traced this to timing issues with asynchronous operations. Their solution was to create custom test helpers that ensured stable test execution by properly awaiting all asynchronous operations and implementing retry mechanisms for network requests.

Practical Exercise

Exercise: Integration Testing a Shopping Cart Workflow

Let's practice by writing integration tests for a shopping cart workflow that includes:

  1. Adding products to the cart
  2. Updating quantities
  3. Applying discount codes
  4. Calculating totals
  5. Processing checkout

You'll need to integrate these components:

  • ProductList component that fetches products from an API
  • CartContext that manages cart state
  • CartSummary component that displays the cart contents
  • CheckoutForm component for submitting the order
  • OrderService that handles order submission to the API

Start with this skeleton:

// shopping-cart-flow.test.js
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

import App from './App';

// Set up MSW server for API mocking
const server = setupServer(
  // Mock product list endpoint
  rest.get('/api/products', (req, res, ctx) => {
    return res(ctx.json({
      products: [
        { id: 1, name: 'Product 1', price: 10.99 },
        { id: 2, name: 'Product 2', price: 24.99 },
        { id: 3, name: 'Product 3', price: 5.99 }
      ]
    }));
  }),
  
  // Mock discount code validation
  rest.post('/api/discounts/validate', (req, res, ctx) => {
    const { code } = req.body;
    
    if (code === 'SAVE10') {
      return res(ctx.json({
        valid: true,
        discountPercent: 10
      }));
    }
    
    return res(ctx.json({
      valid: false,
      message: 'Invalid discount code'
    }));
  }),
  
  // Mock order submission
  rest.post('/api/orders', (req, res, ctx) => {
    // Your mock implementation
  })
);

// Setup and teardown MSW
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('Shopping Cart Flow', () => {
  test('complete shopping flow from browsing to checkout', async () => {
    // Your test implementation
  });
});

Summary

Remember: While unit tests verify that components work correctly in isolation, integration tests give you confidence that they work together as a system.

Assignment

Create a complete integration test suite for an e-commerce workflow:

  1. Create a simplified e-commerce application with these features:
    • Product listing and filtering
    • Product details view
    • Cart management (add, update, remove)
    • Checkout process with form validation
    • Order confirmation
  2. Write integration tests that verify:
    • Products load correctly and can be filtered
    • Users can navigate to product details
    • Products can be added to cart from both listing and details pages
    • Cart quantities can be updated and items removed
    • Checkout form validates user input
    • Order is submitted correctly and confirmation is displayed
  3. Implement proper API mocking with MSW
  4. Include error handling tests (network errors, validation errors)
  5. Document your testing strategy and approach

Bonus challenge: Add user authentication and test authenticated workflows.