fareez.info

Testing React Components Which Has Fetch or Axios API Call

Imagine, you are having a component which is making a REST API call when it is mounted, to fetch data and display it. How to write Unit Test for such a component? In this article we are going to see how to write Unit Tests for such case without any third part utilities other than Jest and react-testing-library.

Note: If you prefer learning through video over blog post, check out the following video otherwise skip and proceed with the text.

const PokemonList = () => {
  const [pokemons, setPokemons] = useState([]);
  const [error, setError] = useState(false);

  useEffect(() => {
    fetch("https://pokeapi.co/api/v2/pokemon")
      .then((resp) => {
        if (resp.status === 200) return resp.json();
        else throw new Error("Invalid response");
      })
      .then((data) => setPokemons(data.results))
      .catch((e) => setError(true));
  }, []);

  return error ? (
    <p>Unable to fetch data</p>
  ) : (
    <table>
      <tbody>
        {pokemons.map((pokemon) => (
          <tr key={pokemon.name}>
            <td>{pokemon.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

This component does the following things

  • Hits the Pokemon API as soon as the it’s mounted
  • If the response status code is 200, then it shows the list of pokemon names
  • Otherwise it shows the error message “Unable to fetch data”

Now we need to test all these three things and make sure the component passes all of them. But the component makes a call to fetch which returns a value which is being used to decide whether to show pokemons or error message. Whenever an external dependency is called by a component we have to mock it when we write an Unit Test.

Can we mock fetch? Yes, we can, but should we? There is a good chance that fetch is being used in multiple places in a single component and mocking it will affect all the places where it’s called. There are workarounds but it will still not be a pleasant experience.

Let’s refactor the code. Extract the fetch call into a separate function in a separate file.

// api.js
export const getPokemonsFromApi = () => {
  return fetch("https://pokeapi.co/api/v2/pokemon").then((resp) => {
    if (resp.status === 200) return resp.json();
    else throw new Error("Invalid response");
  });
};

And now the component becomes

import { getPokemonsFromApi } from "./api";

const PokemonList = () => {
  const [pokemons, setPokemons] = useState([]);
  const [error, setError] = useState(false);

  useEffect(() => {
    // Refactored code
    getPokemonsFromApi()
      .then((data) => setPokemons(data.results))
      .catch((e) => setError(true));
  }, []);

  return error ? (
    <p>Unable to fetch data</p>
  ) : (
    <table>
      <tbody>
        {pokemons.map((pokemon) => (
          <tr key={pokemon.name}>
            <td>{pokemon.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

Jest allows us to mock modules. This is why we extracted the function into a separate module. But now this code also looks a lot more cleaner and meaningful. Remember, whenever you find testing a component to be difficult, then there is a good chance that it can be refactored further to make the dependencies explicit or break down the component into smaller pieces.

Now we have totally abstracted the fetch call behind getPokemonsFromApi. You can even replace fetch with axios, it doesn’t affect how we are going to test the component.

Let’s write a test for the success case.

import { render, screen, waitFor } from "@testing-library/react";
import PokemonList from "./PokemonList";
import * as api from "./api";

/**
 * Mock the api module so that we can inject
 * the desired behavior into getPokemonsFromApi
 * while testing
 */
jest.mock("./api");

describe("PokemonList Component", () => {

  // After each test clear the mock
  beforeEach(() => jest.clearAllMocks());

  it("should render pokemon names when api responds", async () => {
    // For this test, when getPokemonsFromApi is called
    // return the given value wrapped in a Promise
    api.getPokemonsFromApi.mockResolvedValue({
      results: [{ name: "pokedex" }],
    });
    // Render the component
    render(<PokemonList />);
    // See if the pokemon name we returned in the mock is visible
    await waitFor(() => {
      screen.getByText("pokedex");
    });
  });
});

jest.mock allows us to mock the functions within the module. This allows us to call mocking functions like mockResolvedValue that Jest provides.

api.getPokemonsFromApi.mockResolvedValue({
  results: [{ name: "pokedex" }],
});

So now when getPokemonsFromApi is called from within the useEffect of the component, this value will be returned as a Promise. This mock has to be cleared after each test so that the next test is not having the same mock. So we have to call jest.clearAllMocks in the beforeEach hook.

Now to test the case where the Promise throws an error, following test has to be added to the same describe block.

it("should render error message when api fails", async () => {
  api.getPokemonsFromApi.mockRejectedValue({});
  render(<PokemonList />);
  await waitFor(() => {
    screen.getByText("Unable to fetch data");
  });
});

Here mockRejectedValue creates a Promise rejection and so the catch part will run in the useEffect showing the error message.

Here we didn’t use any external libraries to mock the APIs. These tests are more maintainable in the long run than importing a fetch or axios specific mocking library.

comments powered by Disqus