Community for developers to learn, share their programming knowledge. Register!
Testing React Application

Testing API Calls and Asynchronous Code in React


You can get training on this article to better understand the nuances of testing API calls and asynchronous code in React applications. React developers often encounter challenges when writing robust tests for components that interact with APIs. Proper testing ensures that your application works as expected and handles edge cases effectively. In this article, we will explore the intricacies of asynchronous testing and look at tools and techniques to mock API calls, simulate loading states, and validate request parameters.

Testing asynchronous code in React requires careful planning and the right approach, but once mastered, it can significantly improve the reliability of your applications. Let’s dive into the details and learn how to test React applications effectively.

Asynchronous Testing in React

Asynchronous code is at the heart of modern web applications. React components often rely on data fetched from APIs or other asynchronous operations. Testing such code can be tricky because the test execution needs to account for delays, state changes, and potential failures. Unlike synchronous code, where results are immediate, asynchronous operations require the test environment to handle promises, callbacks, or async/await functions.

In React, asynchronous testing is typically done using tools like Jest and React Testing Library (RTL). These tools provide utilities such as waitFor and findBy to help you wait for DOM updates caused by asynchronous actions.

Example:

Here’s an example of testing a component that fetches user data using React Testing Library:

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

jest.mock('axios');

test('displays user data after API call', async () => {
  axios.get.mockResolvedValueOnce({ data: { name: 'John Doe' } });

  render(<UserProfile />);

  // Assert loading state is displayed initially
  expect(screen.getByText(/loading.../i)).toBeInTheDocument();

  // Wait for the user name to be displayed after the API call
  await waitFor(() => expect(screen.getByText(/john doe/i)).toBeInTheDocument());
});

In this example, we mock the API call using Jest and wait for the component to update the DOM. This ensures that the test properly validates the component’s behavior without relying on a real API.

Mocking API Calls with Jest and Axios

Mocking API calls is essential for testing React applications. It allows you to simulate various API responses, such as success, failure, or timeouts, without depending on external services. Jest, combined with libraries like Axios, is a popular choice for mocking API requests.

Why Mock API Calls?

  • Isolation: Ensures tests focus on the component’s logic rather than the API itself.
  • Reliability: Prevents flaky tests caused by network issues or API downtime.
  • Flexibility: Simulate different API response scenarios.

Mocking Example:

jest.mock('axios');

test('handles API error gracefully', async () => {
  axios.get.mockRejectedValueOnce(new Error('Network Error'));

  render(<UserProfile />);

  await waitFor(() => expect(screen.getByText(/error fetching data/i)).toBeInTheDocument());
});

Here, we mock a rejected promise to simulate an API failure scenario. This helps verify that the component handles errors correctly.

Testing Components Before and After API Calls

React components often have different states based on API call results: loading, success, or error. Testing these states ensures your UI behaves as expected.

Example:

  • Before API Call: Validate that the loading state (e.g., a spinner or message) is displayed.
  • After Successful Response: Check if the correct data is rendered in the DOM.
  • After Failed Response: Confirm that an error message or fallback UI is displayed.
test('shows loading, then data, then error', async () => {
  axios.get
    .mockResolvedValueOnce({ data: { name: 'Jane Doe' } })
    .mockRejectedValueOnce(new Error('Failed to fetch'));

  const { rerender } = render(<UserProfile />);

  // Initially, show loading
  expect(screen.getByText(/loading.../i)).toBeInTheDocument();

  // After success
  await waitFor(() => expect(screen.getByText(/jane doe/i)).toBeInTheDocument());

  // Simulate an error scenario
  rerender(<UserProfile />);
  await waitFor(() => expect(screen.getByText(/failed to fetch/i)).toBeInTheDocument());
});

This test ensures that the component transitions through all possible states correctly.

Handling Loading States in Tests

A common pattern in React applications is to show a loading indicator while waiting for API responses. Ensuring that this indicator is displayed during the loading phase is crucial for user experience.

Testing loading states involves asserting that the indicator is present before the API call resolves and removed afterward. The example below illustrates this:

test('displays loading indicator while fetching data', async () => {
  axios.get.mockResolvedValueOnce({ data: { name: 'Alice' } });

  render(<UserProfile />);

  // Assert that loading indicator is visible
  expect(screen.getByText(/loading.../i)).toBeInTheDocument();

  // Wait for loading indicator to disappear
  await waitFor(() => expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument());
});

This approach ensures that your tests accurately reflect the user’s experience.

Simulating Delayed Responses or Timeouts

Simulating delayed responses or timeouts is useful for testing how your component handles slow networks or unresponsive APIs. Jest provides a fakeTimers utility to mock time-based functions like setTimeout or setInterval.

Example:

jest.useFakeTimers();

test('displays timeout message if API call takes too long', async () => {
  axios.get.mockImplementationOnce(() => 
    new Promise(resolve => setTimeout(() => resolve({ data: { name: 'Bob' } }), 10000))
  );

  render(<UserProfile />);

  // Simulate timeout
  jest.advanceTimersByTime(5000);
  expect(screen.getByText(/timeout error/i)).toBeInTheDocument();
});

By simulating delays, you can verify that your application handles slow responses gracefully.

Verifying API Request Parameters and Headers

When testing API calls, it’s important to ensure that requests include the correct parameters and headers. This is especially relevant for APIs that require authentication tokens or query parameters.

Example:

test('sends correct request headers', async () => {
  axios.get.mockResolvedValueOnce({ data: { name: 'Charlie' } });

  render(<UserProfile />);

  expect(axios.get).toHaveBeenCalledWith('/api/user', {
    headers: { Authorization: 'Bearer token' },
  });
});

This test ensures that your API calls are configured correctly and compliant with backend requirements.

Summary

Testing API calls and asynchronous code in React applications requires a solid understanding of tools like Jest, Axios, and React Testing Library. By mocking API requests, simulating network delays, and verifying request parameters, you can ensure your components behave predictably under various scenarios. Additionally, testing loading states and verifying error handling improve the reliability of your UI.

Incorporating these practices into your development workflow will lead to more robust and maintainable React applications. For further learning, refer to the official documentation for Jest and React Testing Library. Keep experimenting with different techniques and test cases to refine your skills in testing React applications!

Last Update: 24 Jan, 2025

Topics:
React