Testing a React application with Vitest

Testing a React application with Vitest

Learn about testing React applications with Vitest. We will cover setting up react with vitest and show how to add unit tests and use the TDD approach to test a react component

Testing a React application with Vitest

React is really popular when creating frontend components and when the scale of your application is increased, we need to have robust tests to cover all scenarios. The unit testing component becomes a great way to ensure the quality of the application and easily find bugs in the build time when the tests are written well. In this post, we will learn how to test a component with React and Vitest

What are we building?

We are going to build a simple accordion component in React and write unit tests in Vitest. The component will have two states. First is the collapsed state which displays only the title. Another state will be the open state which shows the title and the content below the title.

AccordionFinal.png

What is Vite?

Vite is a build tool that is easy to use and fast to compile the react project. We will make use of vite since it is easy to integrate the Vitest testing tool. If you want to know the basics of Vite, we have covered it in this blog post

Why use vitest?

Vitest is really fast and has a good developer experience when used with Vite. We can share the configuration of vite with vitest to make it simple and also ensures that the testing environment is similar to the build environment. Vitest supports HMR which really speeds up your workflow

What is HMR?

HMR stands for Hot Module Reloading. Whenever there is any change to the code, only the changes are updated to the server and the server reflects the new changes

The speed of seeing the changes on your server is improved as we are only sending partial changes for reloading instead of reloading the whole code.

Now that all the jargons are out of the way, let’s see some code that you have come for.

Initialize React project using Vite

We can initialize the project using the following command

npm init vite
cd react-vite-vitest
npm install

ProjectInit.png

Adding Vitest for Testing

We can add Vitest to start adding tests to the project. Install the Vitest as a dev dependency.

npm install -D vitest

Vitest Configuration

One of the advantages of Vitest is that it uses the same config as Vite. This ensures that the test environment is the same as the build environment which increases the reliability of the tests

We will update the config to the following code to add js-dom which helps in testing

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

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
    test: {
      globals: true,
      environment: 'jsdom',
    }
})

Creating an Accordion component

Create a new component called Accordion.tsx and add the following code to create a simple accordion component. It is still incomplete and we will complete it by adding testing first

import React from "react";

type AccordionProps = {
    title: string;
    children: React.ReactNode;
}
const Accordion = (props: AccordionProps) => {

    const {title, children} = props;

    return (
        <div className="accordion">
            <h3 className="accordion-title">{title}</h3>
            <div className="accordion-content">
                {children}
            </div>
        </div>
    );
}

export default Accordion;

ReactAccordionStart1.png

We are just taking the title and children and displaying them. An accordion component should be able to shrink and expand when a button is clicked. So let’s add a test case first for that feature and then implement it.

Creating the test in vitest

Create a new file called Accordion.test.tsx which will contain the test for the Accordion component. Add the following code to that file

import {describe, test} from 'vitest';

describe("Accordion test", () => {
    test("Should show title", () => {

    })
})

Let’s break down the above code

  1. describe - Used to group the test and used to describe what is currently being tested
  2. test - Individual test which is run by Vitest. It can either pass or fail

Here we have not added any test which will return true or false. We will do that shortly after adding the test script

Adding the test script

We need to add the vitest command to the package.json to start the testing script.

"scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest"
  },

Just by calling the vitest command, the test execution will start and it will be in watch mode. This means that any changes being done to the file will re-run the test.

Start the test script

npm run test

InitialTest.png

Since we don’t have any expect statement, the test is considered to be passed

Adding the config for Vitest

We need to have the DOM functionality replicated in the test environment to properly test the react components

JSDom helps in getting that environment for test and so we need to install that as a dev dependency.

We will also make use of testing-library as well which will help in having more utility functions to help test the components. We will get things like render function from this package which will simulate the component being rendered on the browser.

Installing the testing dependecies

npm i -D jsdom @testing-library/react

Adding the config for Vitest

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

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
    test: {
      globals: true,
      environment: 'jsdom',
    }
})

Write a unit test to see if text is visible in vitest

import {describe, expect, test} from 'vitest';
import {render, screen} from '@testing-library/react';
import Accordion from './Accordion';

describe("Accordion test", () => {
    test("should show title all the time", () => {
        
        render(<Accordion title='Testing'><h4>Content</h4></Accordion>);

        expect(screen.getByText(/Testing/i)).toBeDefined()
    })
})

This is the first basic test that makes sure that the title is always displayed on the screen. We are making use of some of the functions from testing-library like render and screen.getByText

getByText returns the element if found otherwise it will throw an exception which will fail the test case.

There are lot more utility functions to choose from based on your use case

https://testing-library.com/docs/react-testing-library/api

Create a test for hiding and showing content

We need to render the component on each test case. We can make use of beforeEach in this case which will run the code inside before each test

import {beforeEach, describe, expect, test} from 'vitest';
import {render, screen} from '@testing-library/react';
import Accordion from './Accordion';

describe("Accordion", () => {

    beforeEach(() => {
        render(<Accordion title='Testing'><h4>Content</h4></Accordion>);
    });

    test("should show title all the time", () => {
       
        expect(screen.getByText(/Testing/i)).toBeDefined()
    })

    test("should not show the content at the start", () => {

        expect(screen.getByText(/Content/i)).toBeUndefined()

    })
})

ContentNotShownTestFail.png

The second test should be failing now because we are expecting that the content should not be shown at the start but we didn’t implement the code to do that. This is a good example of how TDD (Test Driven Development) works. We first write a test that will fail and then implement the functionality to make it pass.

Implementing the logic to pass the test

import React, { useState } from "react";
import './Accordion.css'

type AccordionProps = {
    title: string;
    children: React.ReactNode;
}
const Accordion = (props: AccordionProps) => {

    const {title, children} = props;
    const [show, setShow] = useState(false);

    const onAccordionClick = () => {
        setShow(!show);
    }

    return (
        <div className="accordion">
            <div className="accordion-title">
                <h3>{title}</h3>
                <button onClick={() => onAccordionClick()}>{!show ? 'Show' : 'Hide'}</button>
            </div>
            {show && (
                <div>
                    {children}
                </div>
            )}
        </div>
    );
}

export default Accordion;

We are adding the code to hide and show the content of the accordion. This is achieved by simply changing the state variable of show

We are setting the initial value of show to false which will make the test pass.

Now that we have the basic accordion feature completed, let’s focus on getting more styles using CSS.

Adding the styles for the Accordion

.accordion {
    width: 80vw;
    border: 1px solid gray;
    border-radius: 5px;
}

.accordion-title {
    padding: 0px 25px;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    border-bottom: 1px solid gray;
}

Writing test to validate the open/close behavior

We have completed the functionality of Accordion. Now we can add one more test to see if the accordion opens with a click of the button.

Let’s write the 3rd test like below

fireEvent from the testing library helps in simulating user actions in a unit test. We are using the click method to click the button. This should trigger the accordion to open and then we are awaiting the action to take place. Since that will be async action, we are using the await keyword.

The async unit test will have a default timeout and it will wait till that time. Once the timeout is completed, it will fail the test.

import {beforeEach, describe, expect, test} from 'vitest';
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import Accordion from './Accordion';
import "@testing-library/jest-dom";
import { act } from 'react-dom/test-utils';

describe("Accordion", () => {

    beforeEach(() => {
        render(<Accordion title='Testing'><h4>Content</h4></Accordion>);
    });

    test("should show title all the time", () => {
       
        expect(screen.getByText(/Testing/i)).toBeInTheDocument();
    })

    test("should not show the content at the start", () => {

        expect(screen.queryByText(/Content/i)).not.toBeInTheDocument();
    })

    test("should show the content on accordion click",async () => {

        const title = screen.getByText(/Show/i);
        fireEvent.click(title)

        expect(await screen.findByText(/Content/i)).toBeInTheDocument();
    })
})

AlltestPass.png

Conclusion

We have learned how to write unit tests with Vitest in React. Vitest is still in the beta stage and not yet ready for production use. We think vitest has a huge potential and looks like a good alternative to Jest which is being used by a lot of developers.

Let us know if you have worked with Vitest and any feedback on this post is welcome

Join our Discord - https://discord.gg/AUjrcK6eep