Skip to main content

Mocking API Calls in Tests (Part 2) #152

📖 Introduction

In Mocking API Calls in Tests (Part 1), we learned how to mock the global fetch API to test components that make network requests. This second part expands on API mocking techniques by focusing on mocking JavaScript modules, such as dedicated API service files or libraries like Axios, using jest.mock(). We'll also briefly touch upon using Mock Service Worker (MSW) for a different approach to API mocking.


📚 Prerequisites

Before we begin, ensure you have:

  • Completed Part 1 and understand basic fetch mocking with Jest.
  • A working Jest and React Testing Library setup.
  • Familiarity with JavaScript modules (ES6 import/export).

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Why Mock Modules for API Calls? Encapsulating API logic.
  • Using jest.mock('./path/to/module'): How it works and basic usage.
  • Mocking Specific Functions within a Module: Overriding implementations of exported functions.
  • Testing a Component that Uses an API Service Module.
  • Introduction to Mock Service Worker (MSW): An alternative network-level mocking strategy.
  • Comparing Module Mocking with MSW (Briefly).

🧠 Section 1: Why Mock Modules for API Calls?

While mocking global fetch is effective, in larger applications, it's common practice to encapsulate API call logic into dedicated service modules or use libraries like Axios.

Example API Service Module (src/services/userService.js):

// src/services/userService.js
const BASE_URL = 'https://jsonplaceholder.typicode.com';

export const getUserById = async (userId) => {
const response = await fetch(`${BASE_URL}/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user ${userId}: ${response.status}`);
}
return await response.json();
};

export const getAllUsers = async () => {
const response = await fetch(`${BASE_URL}/users`);
if (!response.ok) {
throw new Error(`Failed to fetch users: ${response.status}`);
}
return await response.json();
};

// You might have other user-related API functions here

Benefits of this approach:

  • Encapsulation: API logic (URLs, headers, error handling specifics) is centralized.
  • Reusability: Components can import and use these service functions without knowing fetch details.
  • Testability: Instead of mocking global fetch everywhere, you can mock this specific userService.js module in tests for components that use it.

Mocking the service module itself allows you to control what these functions return during tests without worrying about their internal fetch calls.


💻 Section 2: Using jest.mock('./path/to/module')

jest.mock(modulePath, factory, options) is Jest's primary mechanism for mocking entire modules.

  • modulePath: A string path to the module you want to mock (relative to the test file or from node_modules).
  • factory (optional): A function that returns the mock implementation for the module. If omitted, Jest automatically creates mock functions for all exports of the module (auto-mock).
  • options (optional): Configuration like virtual: true for virtual mocks.

How it Works (Hoisting): Calls to jest.mock() are hoisted to the top of the code (before any import statements). This means Jest replaces the real module with your mock before any code in your test file (or the component being tested) actually imports it.

Basic Auto-Mocking: If you just call jest.mock('./services/userService'); without a factory function, Jest will:

  1. Find userService.js.
  2. Automatically replace all its exports (e.g., getUserById, getAllUsers) with Jest mock functions (jest.fn()). These auto-created mock functions will return undefined by default. You'll then need to provide specific mock implementations for them in your tests (e.g., userService.getUserById.mockResolvedValueOnce(...)).

🛠️ Section 3: Mocking Specific Functions within a Module

Often, you want to provide a custom mock implementation for the entire module or specific functions within it directly when you call jest.mock(). This is done using the factory argument.

// src/components/UserDetails.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserDetails from './UserDetails'; // Assume UserDetails imports and uses userService
import { getUserById } from '../services/userService'; // Import the real function to mock it

// --- Mock the userService module ---
// This entire block is hoisted to the top by Jest
jest.mock('../services/userService', () => ({
// We are providing a factory function that returns the mock module structure
__esModule: true, // Needed for ES6 modules with default exports, or when mocking named exports
// Mock specific named exports:
getUserById: jest.fn(), // Replace getUserById with a Jest mock function
getAllUsers: jest.fn(), // Also mock getAllUsers if UserDetails uses it (or to be safe)
// If userService had a default export:
// default: jest.fn(),
}));
// --- End of mock ---


describe('UserDetails Component (with mocked userService)', () => {
beforeEach(() => {
// Clear mock history and reset implementations before each test for isolation
// This is important if you provide different mockResolvedValueOnce in tests.
// For jest.fn() created within jest.mock factory, they are reset if the factory re-evaluates,
// but explicit reset is safer.
// If getUserById was imported:
getUserById.mockClear();
// Or if accessing via imported module object (if not destructuring):
// userService.getUserById.mockClear();
// userService.getAllUsers.mockClear();
});

test('fetches and displays user data using userService', async () => {
const mockUser = { id: 1, name: 'Bruce Wayne', email: '[email protected]' };
// Configure the mock implementation for getUserById for this specific test
// Since getUserById is now a jest.fn() due to the mock, we can use mockResolvedValueOnce
getUserById.mockResolvedValueOnce(mockUser);

render(<UserDetails userId="1" />);

expect(screen.getByText(/loading user details/i)).toBeInTheDocument();

// Wait for data to appear
expect(await screen.findByText(mockUser.name)).toBeInTheDocument();
expect(screen.getByText(mockUser.email)).toBeInTheDocument();

// Verify the mocked service function was called
expect(getUserById).toHaveBeenCalledTimes(1);
expect(getUserById).toHaveBeenCalledWith("1");
});

test('displays an error message if userService.getUserById rejects', async () => {
const errorMessage = 'User not found in Batcomputer';
getUserById.mockRejectedValueOnce(new Error(errorMessage));

render(<UserDetails userId="unknown" />);

expect(screen.getByText(/loading user details/i)).toBeInTheDocument();
expect(await screen.findByText(`Error: ${errorMessage}`)).toBeInTheDocument();
expect(getUserById).toHaveBeenCalledWith("unknown");
});
});

// Assume UserDetails.jsx looks something like this:
// import React, { useState, useEffect } from 'react';
// import { getUserById } from '../services/userService';
//
// const UserDetails = ({ userId }) => {
// const [user, setUser] = useState(null);
// const [error, setError] = useState(null);
// const [loading, setLoading] = useState(false);
//
// useEffect(() => {
// if (!userId) return;
// setLoading(true);
// setError(null);
// setUser(null);
// getUserById(userId)
// .then(data => setUser(data))
// .catch(err => setError(err.message))
// .finally(() => setLoading(false));
// }, [userId]);
//
// if (loading) return <p>Loading user details...</p>;
// if (error) return <p>Error: {error}</p>;
// if (!user) return <p>No user selected.</p>;
//
// return ( <div><h1>{user.name}</h1><p>{user.email}</p></div> );
// };
// export default UserDetails;

Explanation:

  1. jest.mock('../services/userService', () => ({ ... }));:
    • This line, placed at the top level of the test file, tells Jest to replace the actual userService module with our defined mock.
    • The factory function () => ({ ... }) returns an object that defines the structure of the mocked module.
    • __esModule: true: This property is often needed when mocking ES6 modules to ensure interoperability, especially if the original module has a mix of default and named exports or is transpiled by Babel.
    • getUserById: jest.fn(): We explicitly say that the getUserById export from our mocked module should be a new Jest mock function.
  2. import { getUserById } from '../services/userService';: Even though we're mocking the module, we still import the function. In the test environment, this getUserById will now refer to the jest.fn() we defined in the mock factory.
  3. beforeEach(() => { getUserById.mockClear(); });: It's good practice to clear mock history (.mockClear()) or reset mock implementations (.mockReset()) before each test to ensure test isolation.
  4. getUserById.mockResolvedValueOnce(mockUser);: Inside each test, we configure the behavior of our mocked getUserById function (which is jest.fn()) for that specific test case.
  5. Assertions: We then assert that our component behaves correctly based on the mocked API response and that getUserById was called as expected.

This approach gives you fine-grained control over the behavior of your API service dependencies during tests.

Mocking Axios (Common Library): If you use Axios, the pattern is similar.

// src/api/axiosInstance.js
// import axios from 'axios';
// export default axios.create({ baseURL: '...' }); // Example

// src/components/MyDataComponent.test.js
import axiosInstance from '../api/axiosInstance'; // Or just 'axios' if using global

jest.mock('../api/axiosInstance', () => ({
__esModule: true,
default: { // If mocking a default export (like an axios instance)
get: jest.fn(),
post: jest.fn(),
}
}));
// Or for global axios: jest.mock('axios'); then axios.get.mockResolvedValueOnce(...)

// ... in your test ...
// axiosInstance.get.mockResolvedValueOnce({ data: { message: 'Mocked with Axios!' } });
// render(<MyDataComponent />);
// ... assertions ...

🔬 Section 4: Introduction to Mock Service Worker (MSW)

While Jest module mocking is powerful, another increasingly popular approach for mocking APIs is Mock Service Worker (MSW).

What is MSW? MSW is an API mocking library that uses Service Worker API to intercept actual network requests from your application (running in a test environment or even in the browser during development) and return mocked responses.

Key Differences from Jest Module Mocking:

  • Network Level vs. Module Level:
    • Jest module mocking replaces your JavaScript modules/functions. Your component calls the mocked function directly.
    • MSW intercepts outgoing fetch or XHR requests at the network level. Your component calls the real fetch or Axios function, but MSW intercepts the request before it hits the network and provides a mocked response.
  • No Import Mocking Needed: You don't need to jest.mock() your API services or fetch. Your application code remains unchanged.
  • Browser and Node Support: MSW can work in both browser environments (for development and E2E tests) and Node.js environments (for Jest tests).

Basic MSW Setup (Conceptual for Jest):

  1. Install MSW: npm install msw --save-dev
  2. Define Request Handlers: You create handlers that specify how to respond to certain requests (e.g., a GET request to /api/users/:id).
    // src/mocks/handlers.js
    import { rest } from 'msw';

    export const handlers = [
    rest.get('https://jsonplaceholder.typicode.com/users/:userId', (req, res, ctx) => {
    const { userId } = req.params;
    if (userId === '1') {
    return res(ctx.status(200), ctx.json({ id: 1, name: 'MSW User' }));
    }
    return res(ctx.status(404), ctx.json({ message: 'User not found via MSW' }));
    }),
    ];
  3. Set up a Mock Server for Node (Jest):
    // src/mocks/server.js
    import { setupServer } from 'msw/node';
    import { handlers } from './handlers';

    export const server = setupServer(...handlers);
  4. Use the Server in Test Setup:
    // src/setupTests.js
    import '@testing-library/jest-dom';
    import { server } from './mocks/server.js';

    beforeAll(() => server.listen()); // Start server before all tests
    afterEach(() => server.resetHandlers()); // Reset handlers after each test
    afterAll(() => server.close()); // Close server after all tests

Now, when your component (like UserProfileFetcher from Part 1) makes a fetch call to https://jsonplaceholder.typicode.com/users/1, MSW intercepts it and returns the mocked JSON, even though the component is using the real fetch.


✨ Section 5: Comparing Jest Module Mocking with MSW (Briefly)

FeatureJest Module Mocking (jest.mock)Mock Service Worker (MSW)
LevelJavaScript module/function levelNetwork request level (via Service Worker)
Code ChangesRequires jest.mock() in test filesApp code unchanged; handlers defined separately
RealismMocks implementation detailsCloser to real network interaction; app uses real fetch/Axios
ScopeTypically per test suite (due to hoisting)Can be global (for dev) or per suite (for tests)
SetupSimpler for individual module mocksMore initial setup (handlers, server)
Use CaseGood for isolating units, mocking specific library functionsExcellent for integration tests, E2E tests, and even development mocking
Dev ExperiencePurely for testingCan be used during development in the browser to mock APIs

When to Choose:

  • jest.mock():
    • Great for unit testing components in isolation when you want to precisely control the behavior of a direct dependency (like a service module).
    • Simpler setup for one-off mocks of small utility modules.
    • When you need to assert that specific mock functions were called with certain arguments.
  • MSW:
    • Excellent for integration tests where you want to test how your components behave with more realistic network interactions without changing their internal API call logic.
    • Provides a consistent mocking layer that can be used across unit tests, integration tests, E2E tests, and even during manual development in the browser.
    • Avoids "mocking fatigue" if many components use the same API endpoints. Define handlers once.
    • Can lead to higher confidence as your app code making the network requests isn't altered for tests.

Many projects benefit from using both. jest.mock() for finer-grained unit test control of specific functions, and MSW for broader API mocking in integration tests or for a consistent dev/test API mock layer.


💡 Conclusion & Key Takeaways (Part 2)

Mocking API calls by mocking entire JavaScript modules (like API services or libraries such as Axios) provides a powerful way to control dependencies in your tests. jest.mock() is the primary tool for this in Jest. For a more network-level approach that keeps your application code unchanged, Mock Service Worker (MSW) offers an excellent alternative that's gaining popularity.

Key Takeaways:

  • Encapsulating API calls in service modules makes them easier to mock.
  • jest.mock('./modulePath', factory) allows you to replace entire modules with mocks, controlling the behavior of their exported functions.
  • Remember that jest.mock calls are hoisted.
  • Mock Service Worker (MSW) intercepts actual network requests, providing a different style of API mocking that can be used across various testing levels and even in development.
  • Choose your mocking strategy based on the test's scope and what provides the most clarity and confidence.

This concludes our series on testing React applications! You now have a foundational understanding of the testing pyramid, setting up Jest & RTL, writing unit tests for rendering and interactions, and mocking API calls.


➡️ Next Steps

Congratulations on completing this comprehensive series on testing in React! This is also the end of Series 19: Testing Your React Application.

We will now move to the final series in Chapter 06: From Development to Production, which is Series 20: Building and Deploying to Production. The first article in that series will be "Creating a Production Build". It's time to learn how to prepare your well-tested application for the world!

Happy (and confident) coding!


glossary

  • jest.mock(modulePath, factory?): A Jest function used to replace a JavaScript module with a mock implementation. Calls are hoisted.
  • Module Mocking: The practice of replacing an entire module with a fake version for testing purposes.
  • Auto-Mocking (Jest): When jest.mock() is called without a factory function, Jest automatically replaces all module exports with jest.fn().
  • Hoisting (in Jest): jest.mock() calls are moved to the top of the test file by Jest's transpiler before execution.
  • Mock Service Worker (MSW): An API mocking library that uses Service Worker API to intercept network requests and return mocked responses.
  • Request Handler (MSW): A function in MSW that defines how to respond to a specific type of network request (e.g., GET to /api/users).

Further Reading