All notes
reactunit testingreact testing libraryjest

React Unit Testing

The unit test simply means that we have to verify the individual and isolated parts work as expected. This can be a component, helper functions, or hooks that we heavily relied on.

·6 min read·65 views

For this guide, we will be using these two library:

  1. Jest - javascript testing framework
  2. React Testing Library - a set of helpers that let us test React components without relying on their implementation details.

Setup

  • We will be using codesandbox for a quick demo
  1. Click here to spin up a react project

  2. Add these devDependencies

    • jest
    • @testing-library/jest-dom
    • @testing-library/react
  3. Create a file index.test.js inside src folder and add this

    import React from "react";
    import { render } from "@testing-library/react";
    import "@testing-library/jest-dom"; // we need this to interact with dom
     
    import App from "./App";
     
    test("should render App", () => {
      const { container } = render(<App />);
     
      expect(container).toHaveTextContent("Hello CodeSandbox");
    });
  4. Click the Tests tab and click play, you should see something like this.

    unit test cli

Congrats 🎉 you made your first test, and it passed!

So recap

  1. We use render from testing-library to render our component

  2. We imported the jest-dom to simulates a DOM environment, and provides a set of custom jest matchers that we can use to extend jest. For this use case, we use the toHaveTextContext on our assertion to check if there is a text content with "Hello CodeSandbox" string.

    The rest of the custom matchers is can be seen here.

Example 1: With Jest Mock Function

Final version https://github.com/mharrvic/unit-test/tree/jest-fetch-mock

Why do we need to mock?

In a unit test, mock objects can simulate the behavior of complex, real objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test.

When do we need to use mock?

  • mock API calls
  • mock databases queries
  • mock conditions difficult to generate in a test environment

Setup

  1. Setup CRA with jest
npx create-react-app unit-test
cd unit-test
  1. Create utils folder and add api-client.js under src folder

  2. Create Contributions.js file under src folder

    import React from "react";
    import { client } from "./utils/api-client";
     
    function ContributorProfile({ total, avatar, username }) {
      return (
        <div data-testid="contributor" className="contributor">
          <div className="profile">
            <img src={avatar} width="60px" alt={username} />
            <p>{username}</p>
          </div>
          <div className="total">
            <p className="number">{total}</p>
            commits
          </div>
        </div>
      );
    }
     
    function Loading() {
      return (
        <div aria-label="loading">
          <p>loading</p>
        </div>
      );
    }
     
    export default function FetchContributors() {
      const [contributors, setContributors] = React.useState({});
      const [status, setStatus] = React.useState("idle");
     
      const fetchContributors = async () => {
        setStatus("loading");
        client("stats/contributors")
          .then((data) =>
            data.map(({ total, author }) => ({
              total: total,
              username: author.login,
              avatar: author.avatar_url,
              id: author.id,
            }))
          )
          .then((contributorList) => {
            setContributors({ contributorList });
            setStatus("success");
          });
      };
     
      return (
        <div>
          <button onClick={fetchContributors}>Fetch contributors</button>
          <>
            {status === "loading" && <Loading />}
            {status === "success" && (
              <div data-testid="contributors" className="contributors">
                {contributors.contributorList.map((contributor) => {
                  return (
                    <ContributorProfile key={contributor.id} {...contributor} />
                  );
                })}
              </div>
            )}
          </>
        </div>
      );
    }
  3. Add the Contributors component to App.js

    import "./App.css";
    import Contributors from "./Contributors";
     
    function App() {
      return (
        <div className="App">
          <Contributors />
        </div>
      );
    }
     
    export default App;
  4. Create __tests__ folder and add fetch-contributor.test.js

    import "@testing-library/jest-dom";
    import {
      fireEvent,
      render,
      screen,
      waitFor,
      waitForElementToBeRemoved,
    } from "@testing-library/react";
    import React from "react";
    import Contributors from "../Contributors";
     
    beforeAll(() => jest.spyOn(window, "fetch"));
     
    test("should render with mock fetch", async () => {
      render(<Contributors />);
     
      window.fetch.mockResolvedValueOnce({
        ok: true,
        json: () => {
          return [
            {
              total: 59,
              author: {
                login: "red",
                id: 1234,
                avatar_url: "https://avatars2.githubusercontent.com/u/1234?v=4",
              },
            },
            {
              total: 122,
              author: {
                login: "leo",
                id: 4567,
                avatar_url: "https://avatars2.githubusercontent.com/u/4567?v=4",
              },
            },
          ];
        },
      });
     
      const fetchButton = screen.getByText("Fetch contributors");
      fireEvent.click(fetchButton);
     
      await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
      await waitFor(() => screen.getAllByTestId("contributor"));
      expect(fetch).toHaveBeenCalledTimes(1);
     
      const contributors = screen.getAllByTestId("contributor");
      expect(contributors).toHaveLength(2);
     
      expect(contributors[0]).toHaveTextContent("red");
      expect(contributors[0]).toHaveTextContent("59");
     
      expect(contributors[1]).toHaveTextContent("leo");
      expect(contributors[1]).toHaveTextContent("122");
    });
    1. Add jest.spyOn that creates a mock and track calls on our fetch API on beforeAll (will trigger on before executing each test)
    2. We added the mockResolvedValueOnce to mock our result once resolved
    3. With the following test interaction, by clicking the Fetch contributions button to trigger fetch
    4. We use the waitForElementToBeRemoved to wait for the loading to be removed on the DOM, and then waiting for getAllByTestId since we added contributor as test-id on the mapped result
    5. Asserts on the expected results
  5. Run yarn test to execute the test

Example 2: With Mock Service Worker(MSW)

Final Version: https://github.com/mharrvic/unit-test/tree/jest-msw

Mock by intercepting requests on the network level. Seamlessly reuse the same mock definition for testing, development, and debugging.

When to use MSW?

Kent C. Dodds published this blog about Stop mocking fetch, which he explains mocking things like fetch is that you end up re-implementing your entire backend, on every test.

Setup (We will going to continue on our progress on example 1)

  1. Add msw dependency

    yarn add msw

  2. Create a test folder under src and add this

    import { rest } from "msw";
    import { setupServer } from "msw/node";
    import { apiURL } from "../utils/api-client";
     
    const handlers = [
      rest.get(`${apiURL}/stats/contributors`, async (_, res, ctx) => {
        return res(
          ctx.json([
            {
              total: 59,
              author: {
                login: "red",
                id: 1234,
                avatar_url: "https://avatars2.githubusercontent.com/u/1234?v=4",
              },
            },
            {
              total: 122,
              author: {
                login: "leo",
                id: 4567,
                avatar_url: "https://avatars2.githubusercontent.com/u/4567?v=4",
              },
            },
          ])
        );
      }),
    ];
     
    const server = setupServer(...handlers);
     
    export * from "msw";
    export { server };
  3. Open the setupTest.js and add this under import "@testing-library/jest-dom";

    import { server } from "./test/test-server";
     
    beforeAll(() => server.listen());
    afterAll(() => server.close());
    afterEach(() => server.resetHandlers());

    This will be trigger on every test. The MSW needs to listen() on every before the test, close() the connection on after all the test, and resetHandlers() on every after each test.

  4. For us to test if the MSW setup is working fine, create a msw-server.test.js under __tests__ folder and add this

    import { server, rest } from "../test/test-server";
    import { client, apiURL } from "../utils/api-client";
     
    test("makes GET requests to the given endpoint", async () => {
      const endpoint = "test-endpoint";
      const mockResult = { mockValue: "VALUE" };
      server.use(
        rest.get(`${apiURL}/${endpoint}`, async (_, res, ctx) => {
          return res(ctx.json(mockResult));
        })
      );
     
      const result = await client(endpoint);
     
      expect(result).toEqual(mockResult);
    });

    Run yarn test to see if it pass, you can playaround with the results

  5. Next we will going recreate the fetch mocking implementation on example 1 by using MSW

    Create a msw-contributor.test.js under __tests__ folder and add this

    import "@testing-library/jest-dom";
    import {
      fireEvent,
      render,
      screen,
      waitFor,
      waitForElementToBeRemoved,
    } from "@testing-library/react";
    import React from "react";
    import Contributors from "../Contributors";
     
    test("using msw", async () => {
      render(<Contributors />);
     
      const fetchButton = screen.getByText("Fetch contributors");
      fireEvent.click(fetchButton);
     
      await waitForElementToBeRemoved(() => [
        ...screen.queryAllByLabelText(/loading/i),
        ...screen.queryAllByText(/loading/i),
      ]);
     
      await waitFor(() => screen.getAllByTestId("contributor"));
     
      const contributors = screen.getAllByTestId("contributor");
      expect(contributors).toHaveLength(2);
     
      expect(contributors[0]).toHaveTextContent("red");
      expect(contributors[0]).toHaveTextContent("59");
     
      expect(contributors[1]).toHaveTextContent("leo");
      expect(contributors[1]).toHaveTextContent("122");
    });

    Looks clean right?

    Under the hood, when the test triggers a click event, MSW intercepts the request and returned our defined output on our test-server.js with the /stats/contributors endpoint.

Resources


https://pawelgrzybek.com/mocking-functions-and-modules-with-jest/

https://en.wikipedia.org/wiki/Mock_object

https://www.richardkotze.com/coding/react-testing-library-jest

https://kentcdodds.com/blog/stop-mocking-fetch

https://mswjs.io/

https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c

More notes