Mocking API Calls in Tests

Loading

When testing React components that rely on external data or API calls, it’s important to mock those API calls so that the tests are predictable, isolated, and run without relying on external services. Mocking API calls allows you to simulate different responses (e.g., successful, error, or loading states) without actually hitting the network.

In this guide, we’ll explore how to mock API calls in your React tests using Jest and React Testing Library.

1. Why Mock API Calls?

Mocking API calls in tests offers several benefits:

  • Isolation: Ensures that your tests only focus on the logic and behavior of your components, not external dependencies like APIs.
  • Faster Tests: API calls can slow down tests, especially when they’re network-dependent. Mocking allows your tests to run faster.
  • Control: You can simulate various scenarios like loading states, successful responses, or errors to see how your component behaves in different situations.

2. Setting Up Jest to Mock API Calls

In a React application, you typically make API calls using fetch, axios, or other libraries. For our example, we’ll focus on mocking fetch API calls using Jest.

3. Example: API Call in a React Component

Let’s assume we have a simple component that fetches user data from an API and displays it.

Example: UserProfile.js

import React, { useState, useEffect } from 'react';

const UserProfile = () => {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/user')
      .then(response => response.json())
      .then(data => setUser(data))
      .catch(error => setError(error));
  }, []);

  if (error) return <div>Error loading user data</div>;
  if (!user) return <div>Loading...</div>;

  return <div>User: {user.name}</div>;
};

export default UserProfile;

In this component:

  • We use useEffect to fetch data when the component mounts.
  • The API call fetches user data from https://api.example.com/user.
  • The component renders a loading message, then the user’s name, or an error message if the API call fails.

4. Mocking the fetch API in Jest

To mock the API call, we’ll use Jest’s mocking capabilities to replace the fetch function with a mock that simulates different responses.

Test: UserProfile.test.js

import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';

// Mock the global fetch function
global.fetch = jest.fn();

describe('UserProfile Component', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('renders loading state initially', () => {
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ name: 'John Doe' }),
    });

    render(<UserProfile />);

    expect(screen.getByText(/Loading.../i)).toBeInTheDocument();
  });

  it('renders user data when the API call is successful', async () => {
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ name: 'John Doe' }),
    });

    render(<UserProfile />);

    // Wait for the user data to load
    await waitFor(() => expect(screen.getByText(/User: John Doe/i)).toBeInTheDocument());
  });

  it('renders error message when the API call fails', async () => {
    fetch.mockRejectedValueOnce(new Error('Failed to fetch'));

    render(<UserProfile />);

    // Wait for the error message to appear
    await waitFor(() => expect(screen.getByText(/Error loading user data/i)).toBeInTheDocument());
  });
});

5. Explanation of the Test

  • Mocking fetch: We use jest.fn() to mock the global fetch function, so it doesn’t make actual network requests during the test. Instead, we control what fetch returns.
  • mockResolvedValueOnce: This tells Jest to return a resolved promise with the provided value the next time fetch is called. In this case, we mock a successful API response by resolving it with user data.
  • mockRejectedValueOnce: This simulates a failed API call by rejecting the promise.
  • waitFor: Since the component is asynchronous (waiting for the API call to complete), we use waitFor to ensure the test waits until the DOM is updated with the results before making assertions.

6. Different API Response Scenarios

When testing components that make API calls, you might want to simulate different scenarios, such as:

  • Successful response
  • Failed response
  • Loading state

Let’s look at how we can mock these scenarios.

Simulating a Successful API Response

In the previous example, we simulated a successful response with the following code:

fetch.mockResolvedValueOnce({
  json: () => Promise.resolve({ name: 'John Doe' }),
});

This simulates a successful response where the json method of the response resolves to { name: 'John Doe' }.

Simulating a Failed API Response

You can simulate an error (e.g., network failure or server error) like this:

fetch.mockRejectedValueOnce(new Error('Failed to fetch'));

This simulates an error response, which will cause the catch block in the UserProfile component to run, rendering the error message.

Simulating a Delayed API Response (Loading State)

To simulate a delay in the API response (e.g., showing the loading state), you can mock fetch with a delay:

fetch.mockResolvedValueOnce({
  json: () => new Promise(resolve => setTimeout(() => resolve({ name: 'John Doe' }), 2000)),
});

This mocks a delayed response, which allows you to test how the component behaves when it’s waiting for data.

7. Mocking API Calls with Axios

If you are using Axios instead of fetch, the approach is similar, but you would mock axios instead. Here’s how you can do it.

Example: Using Axios for API Calls

import axios from 'axios';
import React, { useState, useEffect } from 'react';

const UserProfile = () => {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    axios.get('https://api.example.com/user')
      .then(response => setUser(response.data))
      .catch(error => setError(error));
  }, []);

  if (error) return <div>Error loading user data</div>;
  if (!user) return <div>Loading...</div>;

  return <div>User: {user.name}</div>;
};

export default UserProfile;

Mocking Axios in Jest

import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
import axios from 'axios';

// Mock axios
jest.mock('axios');

describe('UserProfile Component with Axios', () => {
  it('renders loading state initially', () => {
    axios.get.mockResolvedValueOnce({ data: { name: 'John Doe' } });

    render(<UserProfile />);

    expect(screen.getByText(/Loading.../i)).toBeInTheDocument();
  });

  it('renders user data when the API call is successful', async () => {
    axios.get.mockResolvedValueOnce({ data: { name: 'John Doe' } });

    render(<UserProfile />);

    await waitFor(() => expect(screen.getByText(/User: John Doe/i)).toBeInTheDocument());
  });

  it('renders error message when the API call fails', async () => {
    axios.get.mockRejectedValueOnce(new Error('Failed to fetch'));

    render(<UserProfile />);

    await waitFor(() => expect(screen.getByText(/Error loading user data/i)).toBeInTheDocument());
  });
});

8. Best Practices for Mocking API Calls

  • Use jest.clearAllMocks(): After each test, clear mocks to ensure tests don’t interfere with each other.
  • Use Mock Responses to Simulate Real Scenarios: Test various edge cases such as successful responses, failures, and timeouts.
  • Mock External APIs: Always mock external services or APIs in tests to avoid real network requests and ensure test consistency.

Leave a Reply

Your email address will not be published. Required fields are marked *