React component testing with Vitest efficiently
Tags: testing, react, vitest, javascript
Published: 2023-06-21 05:12:55

---
title: "React component testing with Vitest efficiently"
description: "Tips for planning and testing your React component with Vitest and React Testing Library."
published: true
img: "articles/testing/Testing"
tags: ['testing', 'reactjs', 'Vitest', 'javascript']
cover_image: https://res.cloudinary.com/mayashavin/image/upload/v1687309395/articles/testing/react_component_test
canonical_url: "https://mayashavin.com/articles/posts/test-react-components-with-vitest"
series: Unit testings with Vitest
---

_The previous post explored [how to test React Hooks as a standalone unit](https://dev.to/mayashavin/test-your-react-hooks-with-vitest-efficiently-4998) with Vitest and React Testing Library. In this post, we will continue learning how to leverage to unit test React components in a maintainable and extendable way._

## Table of Contents

- [Table of Contents](#table-of-contents)
- [Prerequisites](#prerequisites)
- [Extending Vitest's expect method](#extending-vitests-expect-method)
- [Testing the Movies component with mocks](#testing-the-movies-component-with-mocks)
- [Testing the search input's functionality](#testing-the-search-inputs-functionality)
- [Cleaning up the mocks after each test](#cleaning-up-the-mocks-after-each-test)
- [Cleaning up the DOM after each test](#cleaning-up-the-dom-after-each-test)
- [Summary](#summary)

## Prerequisites

You should have a React project set up and running. The recommended way is to initialize your project with Vite as its bundle management tool, using the command `npm create vite@latest`. 

For testing, we need the following dependencies installed:

* [Vitest](https://vitest.dev/) as the unit testing framework
* [JSDOM](https://github.com/jsdom/jsdom) as the DOM environment for running our tests
* [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) as the React testing utilities 

To do so, we run the following command:
    
```bash
npm install -D vitest jsdom @testing-library/react

#OR
yarn add -D vitest jsdom @testing-library/react
```

In `vitest.config.js` (or `vite.config.js` for Vite projects), we add the following `test` object:

```js
//...
export default defineConfig({
  test: {
    global: true,
    environment: 'jsdom',
  },
})
```

We also add a new `test:unit` command to the `package.json` file for running the unit tests as follows:

```json
"scripts": {
    "test:unit": "vitest --root src/",
}
```

Next, we will do an extra setup to get Vitest to assert DOM elements.

## Extending Vitest's expect method

Vitest offers essential assertion methods to use with `expect` for asserting values. However, it doesn't have the assertion methods for DOM elements such as `toBeInTheDocument()` or `toHaveTextContent()`. For such methods, we can install the `@testing-library/jest-dom` package and extend the `expect` method from Vitest to include the assertion methods in `matchers` from this package.

To do so, we will create a `setupTest.js` file in the root directory of our project and add the following code:

```javascript
/**setupTest.js */
import { expect } from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';

expect.extend(matchers);
```

In `vitest.config.js`, we can add the `setupTest.js` file to the `test.setupFiles` field:

```javascript
//vitest.config.js
/**... */
    test: {
        /**... */
        setupFiles: './setupTest.js',
    },
/**... */
```

And with this setup, `expect()` will now have all the DOM assertion methods needed for testing React components.

Let's look at how to test React components with Vitest and React Testing Library.

## Testing the Movies component with mocks

For this section, we will look at a simple component - `Movies` - that displays a list of movies with the following features:

* The component fetches the movies list from an external source.
* Users can search for a movie by title.

The below screenshot shows how the component should look on the UI:

<img src="https://res.cloudinary.com/mayashavin/image/upload/f_auto,q_auto/v1675680000/articles/testing/movies_ui" loading="lazy" alt="Screenshot showing how the list of movies look like on the browser" class="mx-auto"/>

An example implementation for the `Movies` component is as follows:

```javascript
export const Movies = () => {
    const { movies } = useMovies();
    const {searchTerm, setSearchTerm, filteredItems: filteredMovies} = useSearch(movies);
    return (
        <section>
            <div>
                <label htmlFor="search">Search</label>
                <input 
                    type="search" id="search" value={searchTerm} 
                    data-testid="search-input-field"
                    onChange={event => setSearchTerm(event.target.value)}
                />
            </div>
            <ul data-testid="movies-list">
                {filteredMovies.map((movie, index) => (
                    <li key={index}>
                        <article>
                            <h2>{movie.title}</h2>
                            <p>Release on: {movie.release_date}</p>
                            <p>Directed by: {movie.director}</p>
                            <p>{movie.opening_crawl}</p>
                        </article>
                    </li>
                ))}
            </ul>
        </section>
    )
};
```

We will start by spying and mocking the `useMovies` and `useSearch` hooks with `vi.spyOn()` method from Vitest (with `vi` as the Vitest instance), as in the following code:

```javascript
import * as useMoviesHooks from '../hooks/useMovies';
import * as useSearchHooks from '../hooks/useSearch';


describe('Movies', () => {
    const useMoviesSpy = vi.spyOn(useMoviesHooks, 'useMovies');
    const useSearchSpy = vi.spyOn(useSearchHooks, 'useSearch');
});
```

We will mock their return values using `mockReturnValue` method, as follows:

```javascript
describe('Movies', () => {
    /**... */
    it('should render the app', () => {
        const items = [{
            title: 'Star Wars',
            release_date: '1977-05-25',
            director: 'George Lucas',
            opening_crawl: 'It is a period of civil war.'
        }];

        useMoviesSpy.mockReturnValue({
            movies: items,
        });
    
        useSearchSpy.mockReturnValue({
            searchTerm: '',
            setSearchTerm: vi.fn(),
            filteredItems: items
        });

        /**... */
    });
})
```

Then, we will render the `Movies` component using `render` method from `@testing-library/react` and assert that the component renders the list of movies as expected, as follows:

```javascript
import { describe, it, expect, vi } from 'vitest';
import { Movies } from './Movies';
import { render } from '@testing-library/react';

describe('Movies', () => {
    /**... */
    it('should render the the list of movies', () => {
        /**... */
        const { getByTestId } = render(<Movies />);
        expect(
            getByTestId('movies-list').children.length
        ).toBe(items.length);
    });
})
```

The `getByTestId` method will retrieve the element whose `data-testid` attribute value equals `movies-list`,and we can then assert its children to equal the length of our mocked `items` array.

Using `data-testid` attribute values is a good practice for identifying a DOM element for testing purposes and avoiding affecting the component's implementation in production and tests.

Next, we will test the integration of the search hook in `Movies`.

## Testing the search input's functionality

We start by mocking only the `useMovies` hook to return a set of movies, as follows:

```javascript
it('should change the filtered items when the search term changes', () => {
    const items = [
      { title: 'Star Wars' },
      { title: 'Star Trek' },
      { title: 'Starship Troopers' }
    ];

    useMoviesSpy.mockReturnValue({
      movies: items,
      isLoading: false,
      error: null
    });
});
```

We render the `Movies` component and use the `getByTestId` method to retrieve the search input field using the `data-testid`:

```javascript
it('should change the filtered items when the search term changes', () => {
    /**... */
    
    const { getByTestId } = render(<Movies />);

    const searchInput = getByTestId('search-input-field');
});

```

To test the search functionality in the UI of `Movies`, we will use the two following methods from `@testing-library/react`:

* `fireEvent.change()` - to simulate a user event `change` on the search input field.
* `act()` - to wrap around the execution of user event simulation and ensure all the updates apply to the DOM before proceeding with the assertion on the number of items displayed on the UI.

```javascript
import { fireEvent, render, act } from '@testing-library/react';

it('should change the filtered items when the search term changes', () => {
    /**... */
    act(() => {
      fireEvent.change(searchInput, { target: { value: 'Wars' } });
    })

    expect(
      getByTestId('movies-list').children.length
    ).toBe(1);
});
```

With this, we have tested the user interaction with the search input field and the component's response to the user input.

However, if you run the test after the previous test, this test will fail since the last mock value of `useSearch` is still in effect. We must clean and restore the original implementation after each test to ensure the simulated values are isolated per test case. We will do so in the next section.

## Cleaning up the mocks after each test

To clean up any mocked value of each hook we spied on, we will trigger `mockClear()` as follows:

```javascript
afterEach(() => {
    useMoviesSpy.mockClear();
    useSearchSpy.mockClear();
});
```

With this code, after each test run, Vitest will clear any existing mocked value or implementation of the spied hooks, ready for the next test run. Alternatively, we can use `mockRestore()` to restore the non-mocked implementation.

Next, we will apply a similar approach, cleaning the DOM for each test run but for all the test suites.

## Cleaning up the DOM after each test

In `setupTest.js`, we can run the `cleanup` method from `@testing-library/react` to clean up the DOM after each test, using the `afterEach` method from Vitest, as seen below:

```javascript
/**setupTest.js */
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';

/**... */

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

By doing so, we can ensure that the DOM is clean before each test run, applying it to all the test suites.

## Summary

This article shows us how to test React components using the React Testing Library and Vitest package with the proper mock method and appropriate testing approach.

We can extend the testing from the example tests to cover more scenarios, such as testing a loading state or error state when loading the movies or adding more filter options for the search input. With the right component and hook structures, we can create our testing system in an organized and extendable way.


👉 _If you'd like to catch up with me sometimes, follow me on [Twitter](https://twitter.com/MayaShavin) | [Facebook](https://www.facebook.com/mayashavin/). | [Buy me a coffee](https://www.buymeacoffee.com/VTLRKH6)_

👉 _Learn about Vue with my new book [Learning Vue](https://www.oreilly.com/library/view/learning-vue/9781492098812/). The early release is available now!_