Jan 10, 2023 • 8 min read

Getting Started with Vitest and React Testing Library

author picture
Vadim Korolik
Co-Founder & CTO

While manual testing is sufficient for hobby projects, automated testing becomes increasingly essential as the size of your application grows. There are many different types of automated testing — including unit, integration, and end-to-end testing — and many different parts of your system which should ideally be covered by tests — including the UI, the API layer, the backend business logic, and the data access layer.

This article will show you how to unit and integration test React components. These types of testing are great for reducing regressions and improving confidence in your frontend code.

To begin writing tests, you'll need a test runner and a way to render & interact with your React components. While [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) is the de facto standard for React unit & integration testing, you have many options for your test runner, including Jest, Mocha, Tape, and [Vitest](https://vitest.dev/) — the focus of this article./

Why Vitest?

[Jest](https://jestjs.io/) is the most popular JavaScript test runner and has the largest ecosystem, so why would you use anything else? Having used Jest for many years, I've become quite familiar with its drawbacks:/

- Jest is totally separate from your bundler (e.g. Webpack), so a fair amount of configuration is often needed to get Jest set up in your project. It's common for things that work in your real app to error when running in Jest because of configuration differences.

- Jest transpiles your code using [Babel](https://babeljs.io/) by default, which is slow. Transpiling the code with Babel can often take way longer than actually running your tests. That said, its relatively easy to swap out Babel for the much faster [swc](https://swc.rs/) by using [`@swc/jest`](https://swc.rs/docs/usage/jest)./

- Babel also requires you to write a `.babelrc` configuration file and install several plugins. You'll need three Babel plugins installed just to compile a basic React + TypeScript "Hello World" test.

What if I told you there was a test runner that did not suffer from any of these pitfalls? Enter [Vitest](https://vitest.dev/) — a test runner built on top of the next-generation [Vite](https://vitejs.dev/) bundler. Vitest uses Vite to build your code, so there's no need to configure your bundler and test runner separately. Vite compiles your code using either [esbuild](https://esbuild.github.io/) (the default) or [swc](https://swc.rs/), both of which are extremely fast compilers for JavaScript & TypeScript code./

Vitest uses an API very similar to that of Jest, so you won't need to relearn much at all if you're already familiar with writing Jest tests.

While you _could_ use Vitest even if you bundle your application with [Webpack](https://webpack.js.org/), it may not be worth it since now you are again configuring one build pipeline for the application and another build pipeline for your tests./

What is React Testing Library?

[React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), a.k.a. `@testing-library/react` or RTL, is the standard tool for testing React components in a mock browser environment ([jsdom](https://github.com/jsdom/jsdom))./

To write a React Testing Library test, you'll follow this basic flow:

1. Render the React component you want to test using `render()`.

2. Interact with the React component like an end user would, either using React Testing Library's `fireEvent` API or `@testing-library/user-event`, which aims to simulate real user interactions more accurately.

3. Use the `expect` function to make assertions about the final state of the DOM. For example, if your component is a form, you might want to assert that the component renders the text "Saved!" after the submit button is clicked.

It's important to understand that React Testing Library is a tool for **unit and integration testing React components and is not a full end-to-end testing tool**. While an end-to-end (E2E) test runner like [Playwright](https://playwright.dev/) or [Cypress](https://www.cypress.io/) renders your app in a real browser and enables testing every layer of your frontend and backend code, RTL renders your components in a mock browser environment. Typically, you will mock out any calls to your backend API when writing RTL tests./

While E2E tests offer the most confidence that your application is working correctly, they tend to be much slower and require a much more complex test harness (since you'll need to have your backend running, and reset your database to a known state before each test). As with most things in software development, it's a tradeoff — React Testing Library tests are fast and simple to set up, but E2E tests provide greater confidence. Neither approach is better than the other, and the ideal testing strategy will include both RTL and E2E tests.

Setting Up Your Vite Project

Now that we have covered the background on both Vitest and React Testing Library, it's time to write some code. To bootstrap a new Vite project using npm, open your terminal and run

```shell

npm create vite@latest <project-name>

```

You'll be prompted to choose a framework, language, and compiler. For the framework, you'll need to select React. Whether you use TypeScript or plain JavaScript is up to you — same thing when choosing between esbuild and swc. I'll be using TypeScript and swc for my example project.

Once the project is created, `cd` to the new directory and run `npm install` followed by `npm run dev`. A browser window will open showing your new Vite project. Wasn't that easy!?

![Vite's default React template](images/vite-react-app.png)

The homepage scaffolded by Vite isn't very interesting for writing tests, so let's delete the code in `App` and create a simple form. The form will render a name input and a submit button. If the name input is non-empty when the submit button is clicked, a "Saved!" message appears at the bottom.

![A super simple form created with React](images/form.png)

The code for the form will look something this:

```tsx

export function App() {

const [saved, setSaved] = useState(false);

const [name, setName] = useState('');

return (

<div className="App">

<form

onSubmit={(e) => {

e.preventDefault();

if (name) {

setSaved(true);

}

}}

>

<div className="mb-3">

<label htmlFor="nameInput" className="form-label">

Enter your name:

</label>

<input

id="nameInput"

className="form-control"

value={name}

onChange={(e) => {

setSaved(false);

setName(e.target.value);

}}

/>

</div>

<button type="submit" className="btn btn-primary mb-3">

Save

</button>

{saved && <p className="text-success fw-bold">Saved!</p>}

</form>

</div>

);

}

```

Here, we're using the [Bootstrap CSS](https://getbootstrap.com/docs/5.2/getting-started/download/#cdn-via-jsdelivr) to pretty things up./

Configuring Vitest and React Testing Library

It's almost time to start testing, but first, we need to install Vitest and React Testing Library. You can install everything with

```shell

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom

```

Our tests will utilize `@testing-library/jest-dom` which extends the `expect` API with DOM-aware assertions like `.toBeVisible()` and `.toHaveTextContent()`. Since we're using Vitest instead of Jest, we'll need a tiny bit of setup code to make the package work. Simply create a `src/setupTests.ts` file and add this code [(source)](https://markus.oberlehner.net/blog/using-testing-library-jest-dom-with-vitest/):/

```ts

import matchers from '@testing-library/jest-dom/matchers';

import { expect } from 'vitest';

expect.extend(matchers);

```

Then tell Vitest to run `setupTests.ts` before each test by editing your `vite.config.ts` to look like this:

```ts

/// <reference types="vitest" />

import { defineConfig } from 'vite';

import react from '@vitejs/plugin-react-swc';

// https://vitejs.dev/config//

export default defineConfig({

plugins: [react()],

test: {

environment: 'jsdom',

setupFiles: 'src/setupTests.ts',

},

});

```

The `environment: 'jsdom'` line tells Vitest to run our tests in a mock browser environment rather than the default Node environment.

It's customary to define a `test` script in `package.json`, so that our tests can be run with `npm test`. To do that, simply add

```json

"test": "vitest",

```

to the `"scripts"` section of your `package.json`.

Writing Your First Test

Now it's time to test our form component. Let's write a test that verifies that the "Saved!" message is rendered when the form is submitted and the name input is non-empty. With Vitest, you'll use either `it` or `test` to define your tests, just like you would do with Jest. Let's place the following code in a new file called `App.test.tsx`:

```tsx

it('shows a "Saved!" message when the form is submitted', async () => {

// TODO: Write the test!

});

```

Now we'll walk through writing the test, line-by-line. First, we need to render the component that we're testing, in this case `App`:

```tsx

render(<App />);

```

Then, we need to type something into the name field. We can select the HTML `<input>` element using `screen.getByLabelText` since our `<label>` references the `<input>` via the `htmlFor` attribute.

```tsx

await userEvent.type(screen.getByLabelText('Enter your name:'), 'Sam');

```

Then submit the form by clicking the button:

```tsx

await userEvent.click(screen.getByText('Save'));

```

Notice that we selected the DOM elements by referencing user-visible attributes, specifically the text "Enter your name:" and "Save". This is better than selecting elements using something like `document.querySelector('input[name="name"]')`, since our tests provide the most confidence when they interact with the application the same way a real user would.

Finally, we need to assert that our component behaved as expected after the form was submitted. We'll query the DOM once again and use the `.toBeVisible()` matcher to assert that the element is in the DOM and visible:

```tsx

expect(screen.queryByText('Saved!')).toBeVisible();

```

If you've been following along, your test file will look like this:

```tsx

import { expect, it } from 'vitest';

import { render, screen } from '@testing-library/react';

import userEvent from '@testing-library/user-event';

import { App } from './App';

it('shows a "Saved!" message when the form is submitted', async () => {

render(<App />);

await userEvent.type(screen.getByLabelText('Enter your name:'), 'Sam');

await userEvent.click(screen.getByText('Save'));

expect(screen.queryByText('Saved!')).toBeVisible();

});

```

Now, run the test with

```shell

npm test

```

If everything is set up correctly, your test will pass!

![Vitest showing 1 passed test](images/vitest-pass.png)

Next Steps

While this post describes all the steps necessary to set up Vitest in a Vite-based project, it only scratches the surface when it comes to testing large-scale applications in a meaningful way. As you add more tests to your codebase, you'll need to answer questions like,

- How do you determine which functionality is worth testing? (It's impossible to test everything, and writing tests can be time consuming.)

- How do you mock out communication with your backend?

- How do you keep code duplication down when writing many tests that use the same entities? (Your `User` type may have 20 required properties, and you probably don't want to define all 20 of them in every test that needs a user.)

- Will you use `fireEvent` or `userEvent` to interact with your components? (`userEvent` more accurately simulates the behavior of a real browser, but it can make your tests slower — especially if you are using `userEvent.type` to fill out long forms.)

While these topics are outside the scope of this post, hopefully this has been a helpful introduction to the topic!

Additional Resources

- [Why Vitest?](https://vitest.dev/guide/why.html), from the official Vitest docs./

- [The philosophy behind React Testing Library](https://testing-library.com/docs/react-testing-library/intro#the-problem), from the React Testing Library docs — see the sections titled "The problem" and "The solution"./

Other articles you may like

What is Frontend Monitoring and What Tools Help You Do It?
8 Tips to Help You Maximize Chrome DevTools
InfluxDB: Visualizing Millions of Customers' Metrics using a Time Series Database
Try Highlight Today

Get the visibility you need