Introduction to Testing React Components with Vite, Vitest and React Testing Library
Published: 2024-01-11 14:25:56

_Originally published on my blog: https://bogr.dev/blog/react-testing-intro/_

This article assumes a certain level of agreement between me and you that testing the code we write as developers, is important. And also, that the best way to ensure our code has no bugs, is to write tests and run them as often as possible during the development lifecycle. I'll leave the discussion about the pros and cons of writing tests (and the different approaches in that matter) for another article.

In this article I want to walk you through the setup and writing of your very first test in React. Also, to provide some basic theory around the testing terminology and best practices. Meaning that this article is targeting mostly beginner JS/React developers, but could be a good starting point for any developer at any level of experience.

I hate long intros, so why don't we just dive right in.

## What you're going to learn

- How to setup a React project with Vite, Vitest and React Testing Library
- How to implement a very basic quotes app, which shows a quote from an array and the user is able to navigate go to the next/previous quotes using previous and next buttons.
- What's the difference between **Unit** and **Integration** tests.
- What are some of the most common react-testing-library APIs (`render`, `screen`, `query functions`)
- What's the difference between `get`, `query` and `find` query functions
- The three `A`s testing framework
- Some best practices for writing React tests
- I'll leave some of the tests for you to figure out by yourself. Remember - it's crucial apply the knowledge if you want it to stick.

## Resources

- [GitHub repository](https://github.com/brslv/react-testing-intro) with the code of our project. Feel free to clone it and experiment with it.

## Project setup

### Vite

First things first, let's setup a react project using Vite.

But what's vite, you may ask?

From vite's website:

> Vite (French word for "quick", pronounced /vit/, like "veet") is a build tool that aims to provide a faster and leaner development experience.

We'll use Vite's superpowers to run a simple dev server for our React app while developing. Also, we'll use it to bundle the application later when we're ready to deploy (won't be covered in this article).

#### Create a React project with Vite

Let's start with creating a folder for the app. I'm naming my app `rtl-vite`.

```bash
mkdir rtl-vite && cd rtl-vite
```

Let's now create the React app with Vite. Run this in your terminal:

```bash
npm create vite@latest .
```

You're going to be asked what project scaffold do you want for your new Vite app.

For the sake of simplicity, I've picked the React with JavaScript template, but feel free to select the React with TypeScript one if you want to.

### Installing dependencies

In order to test React apps, we need to bring in couple more libraries to our project.

#### Vitest and React Testing Library

> Vitest is a next generation testing framework powered by Vite.

```bash
npm install -D vitest
```

> The React Testing Library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices.

The RTL is the primary tool we're going to use and interact with in this tutorial.

All the other tools are in a way "the infrastructure" around the testing itself.

So let's install that as well:

```bash
npm install -D @testing-library/react
```

Note: The `-D` flag installs the deps as `dev dependencies`.

### Setting up the test globals and jsdom env

In order to save us some import statements in our test files and tell vitest that we want to use `jsdom`, update the exported config in your `vite.config.js` to the following:

_src/vite.config.js_

```js
export default defineConfig({
  plugins: [react()],
  test: {
    // <--- Add this object
    globals: true,
    environment: "jsdom",
  },
});
```

### Setting up a test script

We have the needed libraries, but how do we run the tests?

Let's add a `test` command to our package.json's scripts section:

_package.json_

```json
"scripts": {
  ...
  "test": "vitest"
},
```

That's it.

We're now ready to roll.

## The app

We need something to test, so let's create a very simple app.

Here's what we're going to build now:

_A minimalist quotes application featuring a streamlined interface with two primary controls: a 'Previous' button and a 'Next' button. The application's central feature is a designated display area where quotes are presented. Upon the user's interaction with the 'Next' button, the application will display a new quote. When the 'Previous' button is clicked, the application will revert to showing the immediately preceding quote._

Let's go.

### The `App` component

So, here's our `App` component.

_src/App.jsx_

```jsx
import { useState } from "react";
import Quote from "./components/Quote";
import Previous from "./components/Previous";
import Next from "./components/Next";

import "./App.css";

export default function App({ quotes }) {
  const [currentIndex, setCurrentIndex] = useState(0);

  const navigate = (direction) => () => {
    let nextIndex = direction === "prev" ? currentIndex - 1 : currentIndex + 1;

    if (nextIndex >= quotes.length) {
      nextIndex = 0;
    }

    if (nextIndex < 0) {
      nextIndex = quotes.length - 1;
    }

    setCurrentIndex(nextIndex);
  };

  return (
    <div className="App">
      <div className="App__quoteContainer">
        <Quote quote={quotes[currentIndex]} />
      </div>

      <div className="App__navigation">
        <Previous onClick={navigate("prev")} />
        <Next onClick={navigate("next")} />
      </div>
    </div>
  );
}
```

To keep things simple, our quotes data is going to be an array with a few items.

The `App` component expects a list of quotes to be passed through its `props`.

Notice also that it keeps a reference to an internal state value called `currentIndex`, which is initially `0`.

The `navigate` function is the meat of our app's functionality. That function gets called when the user navigates back and forth between the quotes.

In there we determine the next index, ensuring it's within the bounds of the quotes array.

The UI consists of the core component (`<Quote />`), followed by the two controls, wrapped in a div that lays them out on the x axis.

### The quotes list

Ideally, our app would interact with some sort of an API to get a list of quotes.

But for the purposes of our exercise, we're going to keep things as simple as possible and define a simple array with a bunch of quotes.

_src/quotes.js_

```js
export const quotes = [
  `“What greater gift than the love of a cat.” — Charles Dickens`,
  `“One of the ways in which cats show happiness is by sleeping.” —Cleveland Amory`,
  `“A cat will be your friend, but never your slave.” —Theophile Gautier`,
  `“No home is complete without the pitter-patter of kitty feet.” —Unknown`,
  `“As every cat owner knows, nobody owns a cat.” —Ellen Perry Berkeley`,
];
```

We're then passing the data to the `App` component like this:

_src/main.jsx_

```jsx
...
  <React.StrictMode>
    <App quotes={quotes} />
  </React.StrictMode>
...
```

### The `Quote` component

The Quote component is a pure functional component, which gets a quote and displays it. That's all.

_src/components/Quote.jsx_

```jsx
import "./Quote.css";

export default function Quote({ quote }) {
  return <blockquote className="Quote">{quote}</blockquote>;
}
```

### The navigation controls

For the navigation controls, we also have two functional components.

_src/components/Previous.jsx_

```jsx
export default function Previous({ onClick }) {
  return (
    <button type="button" onClick={onClick}>
      Previous
    </button>
  );
}
```

_src/components/Next.jsx_

```jsx
export default function Next({ onClick }) {
  return (
    <button type="button" onClick={onClick}>
      Next
    </button>
  );
}
```

## Testing

Since we already have a simple, yet functional app, let's see what we can do to ensure any further development of features in the app goes smoothly and doesn't break any of the existing functionalities.

To do this, we're going to unit test the `Quote`, `Previous` and `Next` components, and also integration test the `App` component.

But before that, some theory.

### Unit tests

In programming, a unit test is a piece of code that is written to test a single application module of a software system.

By module, we usually mean a tiny piece of functionality - a unit - that the program composes with other such pieces in order to do some task.

Just like lego blocks, where each block is a unit/module.

In the React world, you can think a React component as such a module. **Thus, a unit test in React world is designed to test the behavior of a single React component**.

Such components in our application are the `Quote`, `Previous` and `Next`. They are a single-purpose, pure functional components.

We're going to unit test them in a second, but before that, let's quickly remind ourselves what's integration testing.

### Integration tests

Integration tests, are designed to verify that the _interactions_ between two or more single-purpose modules in a software system run smoothly, without any bugs.

In other words, **we test whether a group of units _work together_ (interact) as expected**.

In our application we have the `App` component which composes the rest of the components and that's a perfect candidate for an integration test.

Let's start with the simpler ones - the unit tests.

### Testing the `Quote` component

Our `Quote` component is quite dumb and not much could break there. So it'd be enough to make sure it just renders properly as our first step.

Create a `Quote.test.jsx` file under `src/components/` and paste that in:

```jsx
import { render } from "@testing-library/react";
import Quote from "./Quote";

describe("Quote", () => {
  it("should render properly", () => {
    render(<Quote quote="What a nice day." />);
  });
});
```

So what's actually going on here?

#### The `describe` and `it` functions

Vitest, our testing framework, exposes couple of useful functions which we use to build our tests.

The first one is `describe`. Think of it as a way to group couple of tests that are related in some way or another into a single `describe block`.

Since we're going to write couple of tests that are testing our `Quote` component, I'm going to group them in a single describe block.

You can have as many describe blocks as you wish. Also, you can nest them.

My advice would be to keep things simple. Usually, a single describe block is more than enough.

As you can see from the code above, we put our tests inside of `it` functions. There's also a `test` function, which is basically the same - they're interchangeable.

`it` functions is where you put the actual test code.

##### Are they global?

Recall that in the beginning we added this to our `vite.config.js` file:

```js
test: {
  globals: true,
  environment: 'jsdom',
},
```

What that does is to expose the `describe` and `it` functions on the global object. That means that we can now use those (and couple of other functions) right away, without importing them.

#### `render` is the first step

Take a look at our test again.

```jsx
it("should render properly", () => {
  render(<Quote quote="What a nice day." />);
});
```

All it does is to render our component.

But where does it render it?

Well, the cool thing is it does it behind the scenes and you don't see it. Think of it as an invisible browser.

The `render` function call is the first step of a react test. You pass it the component you want to test, it does it's magic behind the scenes to generate the DOM for that component and after that you can interact with the rendered DOM, as if it was rendered in a browser.

That's the magic of `react-testing-library`.

#### Does it pass?

A test in `Vitest` passes if it doesn't throw an error.

In our case, we never return from our test, so if everything works, it should pass successfully.

Let's run it:

```bash
npm run test
```

And yes, as expected, our first test is successful:

![Quote tests are passing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3revpj40b9crnkh9x6je.png)

### Testing the `Previous` and `Next` components

Now things get a bit more interesting.

Our navigation components take a single `prop`. A function (`onClick`), which we run on button click.

What a good test for those components would be?

A good one would be to test whether or not the function gets called when a button is clicked, right?

Let's do this.

_src/components/Previous.test.jsx_

```jsx
import { expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Previous from "./Previous";

const onClick = vi.fn();

describe("Previous", () => {
  it('should call "onClick" when a button is clicked', () => {
    render(<Previous onClick={onClick} />);

    const button = screen.getByRole("button");

    fireEvent.click(button);

    expect(onClick).toBeCalledTimes(1);
  });
});
```

Lots of things are happening there. Let's go through each of them one step at a time.

#### Mocking the `onClick` function

Just to remind you, our `Previous` and `Next` components get a callback function called `onClick` through their `props`. The the button is clicked, the function is called.

For the purposes of our test, we don't actually care what happens when the button is clicked. We care whether or not the function _gets called_.

A common approach when you want to check if a function/object is being properly used by the system under test is to create a mock function/object and check that expected calls are made to the mock function/object.

In vitest, you can mock a function, by creating a void one using `vi.fn()`.

```js
const onClick = vi.fn();
```

#### _Arrange_ the stage

The first line of our test should be already familiar:

```jsx
render(<Previous onClick={onClick} />);
```

We render the component and pass our mock `onClick` function to it.

But let's examine what happens on the next line:

```js
const button = screen.getByRole("button");
```

It's pretty self-explanatory what that line does. But what's the deal with that `screen` and what's `getByRole`?

The `screen` object is a very important one. You're going to use it in most of your tests. It's always used in conjunction with `render` and both of these objects come from the `react-testing-library` package.

Think of the `screen` object as the "invisible browser", through which you can interact with the rendered DOM inside of the test. That object exposes a bunch of useful methods, which makes selecting elements on the DOM a trivial task.

In our example, we call the `getByRole("button")` to find the button in the rendered DOM.

Here's a list of the most commonly used methods that the `screen` exposes:

```js
// getBy*
getByRole
getByLabelText
getByTestId
...

// getAllBy*
getAllByRole
getAllByLabelText
getAllByTestId
...

// queryBy*
queryByRole
queryByLabelText
queryByTestId

// queryBy*
queryAllByRole
queryAllByLabelText
queryAllByTestId

// findBy*
findByRole
findByLabelText
findByTestId
...

// findAllBy*
findAllByRole
findAllByLabelText
findAllByTestId
...
```

##### What's the difference between `getBy`, `getAllBy`, `queryBy`, `queryAllBy`, `findBy` and `findAllBy` in react-testing-library

We can group all six of these so called _query functions_ in three groups: `get`, `query` and `find` query functions.

We use all of them to find elements on the rendered DOM.

**`getBy`/`getAllBy`**

The `getBy` and `getAllBy` functions do a synchroneus query on the DOM and throw an error if no elements are found, thus failing the test right away. As the name implies, the `getBy` is used to get a single element and the `getAllBy` is used for selecting multiple elements that match the given query.

**`queryBy`/`queryAllBy`**

The `queryBy` and `queryAllBy` do exactly the same, but they **don't throw an error** if no elements are found. That's useful when you need to do some conditional logic inside of your tests, based on the presence of an element, and not fail the test immediately.

**`findBy`/`findAllBy`**

We use the `findBy` and `findAllBy` when dealing with some sort of asynchrony in the component we're testing.

For example, when the component does a `fetch` request to some API and waits for the result to come in order to render a DOM element.

In that scenario, we can use `find` functions to "tell" react-testing-library that the DOM element we're looking for is going to be rendered after some delay (caused by the `fetch` in this case). So, the react-testing-library is going to wait for a certain amount of time for the element to appear and will throw after that time expires and no element is found.

Keep in mind that you need to `await` find operations, which makes the whole test `async`.

Example:

```jsx
it("some async test", async () => { // <--- Notice the async
  render(<SomeComponent />)

  const el = await findByRole("button"); // <--- Notice the await

  ...
})
```

#### `Act`

OK, that's the theory around react-testing-library's `query functions`.

Back to our test.

After we have properly set the "stage" (rendered the component and got a reference to the button), we can proceed to doing the actual click.

For that we're going to need another function from the `react-testing-library` - `fireEvent`.

Again, as the name implies, that's how you trigger an event on a given DOM element.

```js
fireEvent.click(button);
```

Simple, right?

#### `Assert`ing that the button works as `expect`ed

Now to the meat of our test - making sure that what we've done in the first 4 lines actually worked.

That's where the `expect` function comes in handy.

The expect function works like this:

You pass it an object and it gives you back a wrapper around that object, exposing a bunch of useful functions, such as `toBe` or `toEqual` and so on.

In our test, we pass it the `onClick` mock function and `assert` that it's being called once (`toBeCalledTimes(1)`).

```js
expect(onClick).toBeCalledTimes(1);
```

That completes our test.

Now, try to write the test for the `Next` component by yourself. I won't give you the code here, but it should be identical to the one we wrote for the `Previous` component.

The best way to learn thins is to get your hands dirty, so go ahead and try.

### The three `A`s of testing

When writing tests, a good framework to follow is the three `A`s framework.

That stands for **Arrange, Act, Assert**.

Let's take a look at our test for the `Previous` component once again and notice the framework in action:

```jsx
describe("Previous", () => {
  it('should call "onClick" when a button is clicked', () => {
    // Arrange (the stage)
    render(<Previous onClick={onClick} />);
    const button = screen.getByRole("button");

    // Act
    fireEvent.click(button);

    // Assert
    expect(onClick).toBeCalledTimes(1);
  });
});
```

Notice the comments. Those are the three main "blocks" in our test code.

The **Arrange** block is where you do the rendering and querying of elements. Basically, preparing the "stage" for action. Usually, you're going to see `render`s and `screen` operations in that block.

In the **Act** block goes all the code that does something on the stage. Usually, you're going to see some `fireEvent`s here.

The **Assert** is where we validate our assumptions. We do `expects` here.

Some tests may have only Arrange and Assert, some may have only Assert, and others may have all three of them.

This is a universally applicable framework. You can think of your tests in three `A`s regardless of the technology you're writing your tests on.

Now, let's write an integration test.

### Integration testing the `App` component

It's time to make sure that the components that we compose inside of the `App` component interact as expected.

Here's our `App.test.jsx` file. Try to understand what's going on by yourself. In the next few sections we're going to break down each of the three integration tests.

_src/App.test.jsx_

```jsx
import { render, fireEvent, screen } from "@testing-library/react";
import { quotes } from "./quotes";
import App from "./App";

describe("App", () => {
  it("shows first quote on app load", () => {
    render(<App quotes={quotes} />);
    const firstQuote = quotes[0];
    const quote = screen.getByTestId("quote");

    expect(quote.textContent).toBe(firstQuote);
  });

  it("shows next quote on `Next` button click", () => {
    render(<App quotes={quotes} />);
    const secondQuote = quotes[1];
    const quote = screen.getByTestId("quote");
    const nextButton = screen.getByTestId("next-button");

    fireEvent.click(nextButton);

    expect(quote.textContent).toBe(secondQuote);
  });

  it("shows previous quote on `Previous` button click", () => {
    render(<App quotes={quotes} />);
    const firstQuote = quotes[0];
    const quote = screen.getByTestId("quote");
    const nextButton = screen.getByTestId("next-button");
    const prevButton = screen.getByTestId("prev-button");

    fireEvent.click(nextButton);
    fireEvent.click(prevButton);

    expect(quote.textContent).toBe(firstQuote);
  });
});
```

#### Verify the `App` renders the first quote

The first test is going to verify that the `App` component properly initializes the `Quote` component with the first quote from the `quotes` array.

```jsx
it("shows first quote on app load", () => {
  render(<App quotes={quotes} />);
  const firstQuote = quotes[0];
  const quote = screen.getByTestId("quote");

  expect(quote.textContent).toBe(firstQuote);
});
```

As you can see, we don't have an **Act** step here and that's totally fine. We just need to verify the text content of the `quote` element is the proper one.

Did you notice the `getByTestId` call?

##### What is a `testId`?

A `testId` is an easy way to get a handle of a given element in the DOM.

Think of it as an `id`, which you attach to the DOM element.

To make things simple for me, I have just attached `data-testid="quote"` test id to the Quote element, just like this:

_src/components/Quote.jsx_

```jsx
...
    <blockquote className="Quote" data-testid="quote">
      {quote}
    </blockquote>
...
}
```

I also added test ids to the `Previous` and `Next` button components so that it's easier to query them in my tests:

_src/components/Previous.jsx_

```jsx
export default function Previous({ onClick }) {
  return (
    <button type="button" onClick={onClick} data-testid="prev-button">
      Previous
    </button>
  );
}
```

_src/components/Next.jsx_

```jsx
export default function Next({ onClick }) {
  return (
    <button type="button" onClick={onClick} data-testid="next-button">
      Next
    </button>
  );
}
```

#### Verify the quote changes to the next one on `Next` click

```jsx
it("shows next quote on `Next` button click", () => {
  render(<App quotes={quotes} />);
  const secondQuote = quotes[1];
  const quote = screen.getByTestId("quote");
  const nextButton = screen.getByTestId("next-button");

  fireEvent.click(nextButton);

  expect(quote.textContent).toBe(secondQuote);
});
```

What do we have here?

In our _Arrange_ block, we get a reference to the `secondQuote`, which we're going to use in our assert block.

Then, we query the `quote` and the `nextButton` elements, again by using `getByTestId` functions.

In the _Act_ and _Assert_ blocks we trigger a click event on the `nextButton` and then verify that the contents of the `quote` is the same as the `secondQuote`.

Easy peasy, lemon squeezy.

#### Verify the quote changes to the previous one on `Previous` click

```jsx
it("shows previous quote on `Previous` button click", () => {
  render(<App quotes={quotes} />);
  const firstQuote = quotes[0];
  const quote = screen.getByTestId("quote");
  const nextButton = screen.getByTestId("next-button");
  const prevButton = screen.getByTestId("prev-button");

  fireEvent.click(nextButton);
  fireEvent.click(prevButton);

  expect(quote.textContent).toBe(firstQuote);
});
```

This time, in our _Act_ stage, we do a `next` and then `previous` click and verify that the quote is back to the original one.

Pretty straight forward.

### Interactions are crucial

Notice that in our integrations tests we test interactions between 2 or more components.

That's what makes a test an integration one.

### Now it's your turn

I have intentionally left the tests for our array bounds logic. I'll leave that to you to figure out.

The general idea is as follows:

```
1. Test that when the app shows the first quote and the user clicks the "Previous" button, the next quote that's being shown is the last one in the data array.
2. Test that when the app shows the last quote and the user clicks the "Next" button, the next quote that's being shown is the first one in the data array.
```

Got it? Now write the tests. :)

## Conclusion

I hope you had fun reading this article. It came out much longer than expected, but I think I've managed to show you the 80% of what testing a React application looks like.

Of course, we never touched asynchrony, testing hooks and what not, but that was not the idea of this article.

I'll do my best to write such guide on the topics I couldn't cover here, so you can subscribe using the form bellow to get notified when I do.

If you had any questions or comments, feel free to reach out. I'd be happy to discuss.

Take care.