React Unit Testing Tutorial With React Testing Library and Vitest React
React Unit Testing Tutorial With React Testing Library and Vitest React

In this video you will learn how to do React testing with using React Testing Library and Vitest.

When you start testing in React the main question is what tools do you need to use in order to test. We don't get anything from React which helps us testing. This is why we must install additional packages.

jest

The most popular Javascript testing framework is Jest. It's a tool that we use together with React, Angular or Vue quite often. But from my experience in every single project when I need to setup Jest I had some problems.

jest error

Like I need to install Babel, then some dependencies, it was not always clear why Jest was throwing these errors and until I setted it up correctly it was really a pain. After setting up it works just fine and I don't have any complains.

But in this post we are talking about Vitest. I already made a post which allows us to setup a project with most popular frameworks in a matter of seconds.

Now from the same author (who also created Vue) we got Vitest.

vitest

It is also a testing framework just like Jest for example. First of all it is a runner (it can run tests) but also you get things like describe, it, possibility to mock and spy in your tests and much more.

The main point is that it is super fast and if you are using Vite that I highly recommend it makes a lot of sense to look on Vitest and I had zero problems with setting it up.

Initial project

Here I already generated a React project that we will use. First thing that we need to do is install Vitest.

npm i vitest -D

It must be a dev dependency because we don't need it in production.

But this is not all. We also want to use Testing Library.

It's an amazing tool which helps us to write much less code while testing React components.

This is why we need to install 3 packages to work with Testing Library.

npm i @testing-library/jest-dom @testing-library/react @testing-library/user-event -D

Configuring Vitest

Our next step is to tune file vite.config.js which is auto generated by Vite tool and it will have a configuration of Vitest inside.

// vite.config.js
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "./tests/setup",
  },
});

Here we created a test section. By default we don't get describe and it globally like with Jest (which I really like) but if you still want to use it globally without imports you can set it to true. We also provided here a path for the setup file that we must create.

// tests/setup.js
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";

afterEach(() => {
  cleanup();
});

Here we specified that after each test we want to call a cleanup method from testing library.

It unmounts React components that were mounted during testing.

The last change that we must do is create a script to test with Vitest.

"scripts": {
  ...
  "test": "vitest"
}

Basic component testing

Now let's start with some basic testing. In order to do that I already prepared a small component.

//src/errorMessage/errorMessage.jsx
const ErrorMessage = ({ message = "Something went wrong" }) => (
  <div data-testid="message-container">{message}</div>
);

export default ErrorMessage;

As you can see this is an extremely small component with just a single prop message then we simply render this message inside a div. The only difference that you won't see in normal component is this data-testid attribute.

This is how we want to find element inside our test. Typically programmers might remove classes but not such unique attributes.

This is why we will use data-testid in all places where we need to find elements.

Now let's create our first test. We must create a file with suffix test so Vitest can find our test.

// src/errorMessage.test.jsx
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import ErrorMessage from "./ErrorMessage";

describe("ErrorMessage", () => {
  it("renders default error state", () => {
    render(<ErrorMessage />);
    screen.debug()
  });
});

As you can see it import describe and it blocks from vitest so they are not global like in Jest. We use render function to render our component for testing.

Screen debug

This is what we see in terminal. screen.debug allows us to see how our component is rendered to understand if what we are writing is correct.

it("renders default error state", () => {
  render(<ErrorMessage />);
  expect(screen.getByTestId("message-container")).toHaveTextContent(
    "Something went wrong"
  );
});

Here we used scree.getByTestId which gets an element by attribute that we created. If we didn't provide a message then our default message should be rendered inside.

I also highly recommend you to always provide a wrong value in expect first to recheck that your test really fails.

Now let's check if we can provide a custom message to our component.

describe("ErrorMessage", () => {
  ...
  it("renders custom error state", () => {
    render(<ErrorMessage message="Email is already taken" />);
    expect(screen.getByTestId("message-container")).toHaveTextContent(
      "Email is already taken"
    );
  });
});

We just provide inside render props of the component and write an expectation. As you can see our tests are green.

green tests

Testing React props

Here for you I have one more component which is pagination and it is more difficult.

import { range } from "../utils";
const Pagination = ({ total, limit, currentPage, selectPage }) => {
  const pagesCount = Math.ceil(total / limit);
  const pages = range(1, pagesCount + 1);

  return (
    <ul className="pagination">
      {pages.map((page) => (
        <li
          data-testid="page-container"
          key={page}
          onClick={() => selectPage(page)}
          className={`page-item ${currentPage === page ? "active" : ""}`}
        >
          <span className="page-link">{page}</span>
        </li>
      ))}
    </ul>
  );
};

export default Pagination;

We get 4 props inside, we prepage a list of pages with a helper range and we render a list of these pages. Let's write our test.

describe("Pagination", () => {
  it("renders correct pagination", () => {
    render(<Pagination total={50} limit={10} currentPage={1} />);
    expect(screen.getAllByTestId("page-container").length).toBe(5);
    expect(screen.getAllByTestId("page-container")[0]).toHaveTextContent("1");
  });
});

We do exactly the same like in previous component. We pass inside needed props and check what what rendered in the component. The only difference here is that we don't have a default state of the component as it can't exist without props.

Testing React callback

But now we have something more interesting. We want to test that user can really click on the page and we get a callback outside.

describe("Pagination", () => {
  ...
  it("should emit clicked page", () => {
    const handleClick = vi.fn();
    render(
      <Pagination
        total={50}
        limit={10}
        currentPage={1}
        selectPage={handleClick}
      />
    );

    fireEvent.click(screen.getAllByTestId("page-container")[0]);
    expect(handleClick).toHaveBeenCalledOnce();
  });
});

Here we created handleClick which is vi.fn. What is vi.fn? This is a special function for testing which doesn't do anything but we can check if it was called on not. Our fireEvent.click simulated clicking on the page and after that we check if our callback was called.

Mocking dependencies

But here is one problem. We still used range function helper in our pagination in order to generate a list of pages. It means that we didn't test our component in isolation and we have a dependency outside.

We typically want to test everything in isolation to make sure that dependencies don't change results of the tests.

describe("Pagination", () => {
  beforeEach(() => {
    vi.mock("../utils", () => {
      return {
        range: () => [1, 2, 3, 4, 5],
      };
    });
  });

  afterEach(() => {
    vi.clearAllMocks();
  });
  ...
});

We didn't change our tests here but we mocked our dependency file with vi.mock. Instead we created just an object with range function which returns plain data. Also we should not forget to clear our mock after each test with clearAllMocks.

It won't change the results of our tests but now utils dependency will never be used during our tests.

Testing plain functions

We already tested all our components but we didn't test our utils file that we mocked. We must test it separately to be on the safe side.

// src/utils.js
export const range = (start, end) => {
  return [...Array(end - start).keys()].map((el) => el + start);
};

As you can see it contains just a single function range that we use inside our pagination. Let's test it now.

import { describe, expect, it } from "vitest";
import { range } from "./utils";

describe("utils", () => {
  describe("range", () => {
    it("returns correct result for 1-6 range", () => {
      const result = range(1, 6);
      const expected = [1, 2, 3, 4, 5];
      expect(result).toEqual(expected);
    });

    it("returns correct result for 41-45 range", () => {
      const result = range(41, 45);
      const expected = [41, 42, 43, 44];
      expect(result).toEqual(expected);
    });
  });
});

Here we wrote 2 tests for our range function. We don't need any helpers or additional libraries because utils are just plain Javascript functions.

And actually if you want to improve your React knowledge and prepare for the interview I highly recommend you to check my course React Interview Questions.

📚 Source code of what we've done