-2

I'm working on a react+ts app, where one can search for users using the gitHub API.

My search input looks like this :

<input 
    type="text" 
    placeholder="Search users by name" 
    className={styles.toolbar__searchInput}
    onChange={handleChange}
/>

And the handleChange function is debounced to avoid making too many requests. I'm also checking that the response is OK :

const handleChange = debounce(async (event:React.ChangeEvent<HTMLInputElement>) => {

    try {
        const response = await fetch(`https://api.github.com/search/users?q=${event.target.value}`);

            if (!response.ok && response.status === 403) {
              throw new Error('API rate limit exceeded');
            }

            const users = await response.json();
            props.setResults(users.items);
        } catch (error) {
            console.error(error);
        }
});

When the fetch succeeds, users are saved in the app state with props.setResults().

Now I'm trying to test the fetch Response with jest, but I get an error like TypeError: Cannot read properties of undefined (reading 'ok')

My test looks like :

(global as any).fetch = jest.fn(async() =>
  Promise.resolve({
    "ok": true,
    "status": 200,
    json: () => Promise.resolve({
      "items": [
        {
          "login": "roxxorDev1",
          "id": 12345,
          // ...
        }
      ],
    })
  })
);

test.only('typing in the search input triggers a fetch', async () => {
  jest.useFakeTimers();
  render(<App />);
  
  const searchInput:HTMLInputElement = screen.getByPlaceholderText('Search users by name');

  searchInput.focus();
  userEvent.keyboard('roxxorDev1');
  searchInput.dispatchEvent(new Event('change'));

  jest.runOnlyPendingTimers();

  const user = screen.getByText('roxxorDev1');

  expect(user).toBeInTheDocument();
});

How should I proceed ?

bnthor
  • 3
  • 1
  • [SO search results for `react jest mock fetch`](https://stackoverflow.com/search?q=react+jest+mock+fetch) – jsejcksn Jul 25 '23 at 08:03
  • Does this answer your question? [How to mock fetch response using jest-fetch-mock](https://stackoverflow.com/questions/72718623/how-to-mock-fetch-response-using-jest-fetch-mock) – jsejcksn Jul 25 '23 at 08:04

1 Answers1

0

In your code, you're currently using fetch = jest.fn() to create the mock. But there is a problem: fetch isn't a function in jest’s eyes, rather it's a property in the global object, which causes the error you're seeing. Also, the response should be implemented as a function, hence it's better to use mockResolvedValueOnce.

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => 
      Promise.resolve({
        items: [
          {
            login: 'roxxorDev1',
            id: 12345,
            // other properties
          },
        ],
      }),
  })
);

Let's use jest.spyOn instead and mockImplementationOnce to return a promise. When using mockImplementationOnce, you provide it a new function that will be used as the implementation of the mock for one run only:

jest.spyOn(global, 'fetch').mockImplementationOnce(() => 
  Promise.resolve({
    ok: true,
    status: 200,
    json: () => Promise.resolve({
      items: [
        {
          login: 'roxxorDev1',
          id: 12345,
          // ...
        },
      ], 
    }),
  })
);

The modified jest.spyOn call acts as a spy while also replacing the fetch implementation with the provided function, which ensures that ok, status, and json properties are correctly mocked.

We also need to update the jest calls to handle asynchronous actions:

test.only('typing in the search input triggers a fetch', async () => {
  jest.useFakeTimers();
  
  jest.spyOn(global, 'fetch').mockImplementationOnce(() => 
    Promise.resolve({
      ok: true,
      status: 200,
      json: () => Promise.resolve({
        items: [
          {
            login: "roxxorDev1",
            id: 12345,
            // ...
          }
        ],
      })
    })
  );

  render(<App />);
  
  const searchInput: HTMLInputElement = screen.getByPlaceholderText('Search users by name');
  await userEvent.type(searchInput, 'roxxorDev1');

  // allow any pending promises to be resolved
  await act(() => new Promise(resolve => setTimeout(resolve, 0)));
  
  const user = screen.getByText('roxxorDev1');
  expect(user).toBeInTheDocument();

  // clean up and restore the original (non-mocked) fetch implementation
  (global.fetch as jest.Mock).mockRestore();
});

Using await act(() => new Promise(resolve => setTimeout(resolve, 0))); will allow any pending promises to finish before checking for the user element.

This approach of using jest.spyOn for mocking fetch allows jest to keep track of all calls to the function, so you can inspect them later (for example, to assert how many times the function was called or what arguments it was called with), while mockImplementationOnce gives the mock a one-time return value.

Kozydot
  • 515
  • 1
  • 6
  • Excellent! Thanks for the solution. Although I had to change a line. The `await act()` callback function triggered an "Exceeded timeout of 5000ms" error. I used this instead, and it works => `await act(() => { jest.runOnlyPendingTimers(); });` – bnthor Jul 25 '23 at 12:24