Mastering Asynchronous React Testing: A Jest Strategy for Functions with Multiple Async Calls
Tags: jest, react, reactnative, async
Published: 2024-03-02 23:52:26

Hey React developers! 🚀

Today, let's dive into the world of asynchronous testing in React applications using Jest. Asynchronous operations are a fundamental aspect of modern web development, especially when dealing with data fetching, API calls, and other async tasks. In this post, we'll explore a robust testing strategy for handling multiple async awaits within a function using Jest and React Testing Library.

Consider a scenario where you have a React component that fetches data from an API asynchronously and renders the results. Here's a sample component along with its testing strategy:

```js
// FetchDataExample.js
export function FetchDataExample() {
  const [results, setResults] = useState([])

  const handleFetchData = async () => {
    try {
      const data1 = await fetcher('https://jsonplaceholder.typicode.com/posts/1')
      setResults((prevResults) => [...prevResults, data1])

      const data2 = await fetcher('https://jsonplaceholder.typicode.com/posts/2')
      setResults((prevResults) => [...prevResults, data2])

      const data3 = await fetcher('https://jsonplaceholder.typicode.com/posts/3')
      setResults((prevResults) => [...prevResults, data3])

      const data4 = await fetcher('https://jsonplaceholder.typicode.com/posts/4')
      setResults((prevResults) => [...prevResults, data4])


    } catch (error) {
      console.error('Error fetching data:', error)
    }
  }

  return (
    <div>
      <h1>Fetch Data Example</h1>
      <button onClick={handleFetchData}>Fetch Data</button>
      <div>
        {results.map((result, index) => (
          <div key={index}>
            <h2>Result {index + 1}</h2>
            <pre>{JSON.stringify(result, null, 0)}</pre>
          </div>
        ))}
      </div>
    </div>
  )
}
```
```js
// FetchDataExample.test.js
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { FetchDataExample } from '../FetchDataExample';

const mockFetcher = jest.fn();

jest.mock('../fetcher', () => ({
  fetcher: () => mockFetcher()
}));

const mockPromiseWithTimeout = (timeout, value) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(value);
    }, timeout);
  });
};

describe('Testing multiple promises in a function call', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  it('fetches data and renders results', async () => {
    mockFetcher
      .mockImplementationOnce(() => mockPromiseWithTimeout(10, { test: '1' }))
      .mockImplementationOnce(() => mockPromiseWithTimeout(10, { test: '2' }))
      .mockImplementationOnce(() => mockPromiseWithTimeout(10, { test: '3' }))
      .mockImplementationOnce(() => mockPromiseWithTimeout(10, { test: '4' }))

    const { getByText, queryByText } = render(<FetchDataExample />);

    fireEvent.click(getByText('Fetch Data'));

    await waitFor(() => {
      expect(getByText('Result 1')).toBeInTheDocument();
      expect(getByText('{"test":"1"}')).toBeInTheDocument();
      expect(queryByText('Result 2')).toBeFalsy();
      expect(queryByText('{"test":"2"}')).toBeFalsy();
      expect(queryByText('Result 3')).toBeFalsy();
      expect(queryByText('{"test":"3"}')).toBeFalsy();
      expect(queryByText('Result 4')).toBeFalsy();
      expect(queryByText('{"test":"4"}')).toBeFalsy();
    });

    await waitFor(() => {
      expect(getByText('Result 2')).toBeInTheDocument();
      expect(getByText('{"test":"2"}')).toBeInTheDocument();
      expect(queryByText('Result 3')).toBeFalsy();
      expect(queryByText('{"test":"3"}')).toBeFalsy();
      expect(queryByText('Result 4')).toBeFalsy();
      expect(queryByText('{"test":"4"}')).toBeFalsy();
    });

    await waitFor(() => {
      expect(getByText('Result 3')).toBeInTheDocument();
      expect(getByText('{"test":"3"}')).toBeInTheDocument();
      expect(queryByText('Result 4')).toBeFalsy();
      expect(queryByText('{"test":"4"}')).toBeFalsy();
    });

    await waitFor(() => expect(getByText('{"test":"4"}')).toBeInTheDocument());
  });
});
```

In this example, we're testing a React component FetchDataExample that fetches data asynchronously using a mock fetcher function. We're mocking the fetcher function to return promises with specific data after a timeout.

To test the component's behavior, we're simulating a user clicking on the "Fetch Data" button and then waiting for the asynchronous data fetching and rendering to complete using waitFor from React Testing Library.

The magic in this strategy is if we are able to test the in-between states, using `mockPromiseWithTimeout`. 

The testing strategy involves:

1. Mocking the asynchronous `fetcher` function with `mockPromiseWithTimeout`, the utility function I created. 
2. Simulating user interaction with the component.
3. Asserting that the component renders the expected data after each async operation using `waitFor`.

By following this strategy, we ensure that our React components behave correctly even with complex asynchronous operations.

Utility Function Explanation:

In the testing strategy, we utilize a utility function called `mockPromiseWithTimeout` to simulate asynchronous behavior with sequenced and controlled timing. Normally, you would `mockResolvedValueOnce` but this does not work if you want to test what is being rendered in the dom in between calls. 

```js
const mockPromiseWithTimeout = (timeout, value) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(value);
    }, timeout);
  });
};
```

This utility function is particularly useful in testing scenarios where you need to simulate asynchronous behavior, such as API calls, or for react native async functionality, that take some time to complete. By using `mockPromiseWithTimeout`, you can control the timing of when the Promise resolves, allowing you to test how your application behaves before, after, and during async functions.

Happy testing! 🧪✨

Alex Matheson
[linkedin](https://www.linkedin.com/in/alexkev)
[X](https://twitter.com/alexkevhikes)