Answering questions about using Vitest and React Testing Library for frontend testing

Gideon Akinsanmi - Sep 5 - - Dev Community

In this article, I’ll be taking a different approach to show you how to run tests on your React app with Vitest and React testing library. This article answers the frequently asked question about using Vitest and React testing library. Before starting, you must ensure that you have a basic knowledge in writing code in React and TypeScript. At the end of this tutorial, I’m sure you’ll have a better understanding of how Vitest and React testing library works.

Table of Content

What is Vitest?

Vitest is a testing framework built on top of Vite. It is used to test code written in JavaScript and TypeScript. It supports integration with various tools across the JavaScript ecosystem.

How do I set up Vitest in React?

If you installed React with Vite, all you need is to install Vitest, Jsdom, React testing library and Jest DOM as development dependencies.

npm install --save-dev vitest jsdom @testing-library/react @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

vitest will be used to run the tests.

jsdom will be used to simulate a browser environment for running the tests.

@testing-library/react will be used to render the React components, query elements, and interact with them.

@testing-library/jest-dom has a list of matchers that simplifies the conditions to be tested on the React components.

Once that is installed, you need to configure the environment to use jsdom. In your vite.config.ts file, create a test object with an environment of 'jsdom'.

// vite.config.ts file
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom'
  }
});
Enter fullscreen mode Exit fullscreen mode

Although you can have a separate configuration file for Vitest, it’s always advised to use the same file since Vitest was built on top of Vite.

Can Vitest be used in frontend and backend testing?

Yes, Vitest can be used to test frontend and backend code.

Vitest can be used to test components of frontend frameworks like React, Vue, Angular, Svelte, etc. It can be used to test how components behave in response to changes in props, state, and user interactions.

Before running the tests, you need to configure the environment to one that simulates a browser environment in the vite.config.ts (or vitest.config.ts) file. The value can either be jsdom or happy-dom library. Once this is specified, you'll be able to interact with the DOM and use any browser web API in the test.

For backend, it can be used with other Node.js libraries to test backend logic, API endpoints, database connections, and external services. The environment must be configured to node to access Node.js-specific features.

How do I create and run tests in Vitest?

Before creating a test, you must have a functionality to test. It can be a function that performs an operation or a component renders some element. After that, you must create another file that'll contain the test for the function. The test file must always end with .test.ts(or .test.tsx). If the function is in a func.ts file, the test will be in a func.test.ts file.

For example, Let’s say you have a add function in a file (add.ts)

export default function add(a:number, b:number): number{
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

You can write a test (in an add.test.ts file) that asserts that the addition of any 2 numbers will return the sum of the numbers:

//add.test.ts
import { test, expect } from 'vitest';
import add from './add'; //import the function

test('addition of two numbers gives the sum', () => {
    expect(add(2,4)).toBe(6);
})
Enter fullscreen mode Exit fullscreen mode

From the code above the test expects the addition of 2 and 4 to be 6. If the result of add(2,4) is the same as the expected value(6), the test will pass. If not, the test will fail.

To run the tests, you must have a script in the package.json that runs Vitest. ‘vitest’ runs the tests once while ‘vitest – watch’ runs Vitest in watch mode.

"scripts": {
  "test": "vitest",
  "test:watch": "vitest --watch"
}
Enter fullscreen mode Exit fullscreen mode

When you run npm test command, it’ll run all the files ending with (.test.ts) once.

When you run the npm test:watch command, it’ll run Vitest in watch mode and any code changes will automatically trigger a rerun of the test.

After running npm run test in the CLI, this is the result I got for the test:

Test for add function passed

From the image above, the test passed!

If you change the test to expect that the addition of 2 and 4 gives 5, the test will fail. This is because the addition of 2 and 4 isn't 5.

//add.test.ts
import { test, expect } from 'vitest';
import add from './add';

test('addition of two numbers gives the sum', () => {
    expect(add(2,4)).toBe(5);
})
Enter fullscreen mode Exit fullscreen mode

Result:
Test for add function failed

What are the most used functions/methods in Vitest?

They include describe(), it(), test(), expect(), toBe(), not(), and so on.

describe()

describe() is used to group all related test cases for a function/component together.

For example, you can have 2 test cases for the add function. One tests that the addition of 2 numbers is equal to the sum of the numbers and the second tests that the addition of 2 numbers isn't the same as the wrong result.

import {describe, it, test, expect } from 'vitest';
import add from './add';

describe('add function', () => {
  it('adding 2 numbers equals the sum', () => {
    expect(add(2,4)).toBe(6); //the test will pass because the addition of 2 and 4 is 6
  })

  it('adding 2 numbers is not equal to the wrong sum', () => {
    expect(add(2,4)).not.toBe(5); //the test will pass because the addition of 2 and 4 is not 6
  })  
})
Enter fullscreen mode Exit fullscreen mode

Result:

Tests in the suite passed

it() and test()

it and test functions represent a single test case.

expect()

expect is used to test assertions (whether a result matches your expected result).

toBe()

toBe is one of the expectations or matchers used with the expect function.

It’s used to check whether the computed value is strictly equal (===) to the predicted value. If it turns true, the test passes. If it’s false the test fails.

For example, in the add.test.ts file, we expected the addition of 2 and 4 to be 6.

not()

not is used to confirm that a condition is not true. For example, in the add.test.ts function, we expected that the addition of 2 and 4 should not be equal to 5.

test('addition of two numbers is not equal to the wrong sum', () => {
  expect(add(2,4)).not.toBe(5);
})
Enter fullscreen mode Exit fullscreen mode

How do I check my test results in the browser?

Vitest has a UI mode for checking the test results in a browser-based user interface.

To set this up, you need to install Vitest UI.

npm install –save-dev @vitest/ui
Enter fullscreen mode Exit fullscreen mode

After that, you should add another script (vitest --ui) in your package.json file that runs the test in UI mode.

 "script": {    
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui"
}
Enter fullscreen mode Exit fullscreen mode

And finally, you enter npm run test:ui in your CLI to run the script.

After a few seconds, a page will be opened in your browser, and you’ll be able to see more information about your tests.

Vitest UI

With Vitest UI, you can easily check your test results in watch mode, filter tests, trace, and fix errors.

What is the difference between testing React components and plain JavaScript/TypeScript code?

If you’re testing a JavaScript or Typescript function, the code and test must have the same extension, ending with either .js or .ts depending on your configuration (add.js and add.test.js, add.ts and add.test.ts).

Also, code that contains React components must have the same extensions with their test (eg Hello.tsx and Hello.test.tsx).

If the component and test file have different extensions, you won't be able to run the test. (eg Hello.tsx and Hello.test.ts will return an error).

What are the most common functions/objects in the React testing library?

The most used functions/objects are the ones that render, query, and interact with the DOM. They include render(), screen, getBy, and queryBy methods, toBeInTheDocument(), fireEvent., and so on.

render function

The render function renders the component that’ll be tested in a virtual DOM. Before testing any React component, you first need to render it in a virtual DOM. After that, you can query any element you like.

For example, if you have a component that renders a heading with some text. To test that it renders correctly, you first need to render it with the render function.

//Hello.tsx
export default function Hello() {
    return <h1>Hello world</h1>;
};

//Hello.test.tsx
import {describe, it} from 'vitest';
import {render} from '@testing-library/react';
import Hello from './Hello';

describe('Hello component', () => {
    it('renders component', () => {
        render( <Hello />);
        //code to test that the heading renders
    })
})
Enter fullscreen mode Exit fullscreen mode

screen object

screen is a utility object that has different methods for querying the elements in the DOM. It contains query methods like getByRole(), getByText(), and so on.

For example, if you have a component that displays a message when rendered, you can test that the specific element is rendered.

Hello.tsx
export default function Hello() {
    return <h1>Hello world</h1>;
};

//Hello.test.tsx
import {describe, it} from 'vitest';
import {render, screen} from '@testing-library/react';
import "@testing-library/jest-dom/vitest";
import Hello from './Hello';

describe('Hello component', () => {
    it('renders heading', () => {
        render( <Hello />); //renders the component
        let heading = screen.getByRole('heading'); //selects the heading element
        expect(heading).toBeInTheDocument(); //the test will pass if the heading is rendered in the DOM
    })
})
Enter fullscreen mode Exit fullscreen mode

getBy and queryBy methods

getBy and queryBy are query methods used to select an element based on some conditions. There are different variants of these query methods which includes getByRole(), getByText(), getByTestId(), and so on.

For example, if you have a component that renders some content based on whether the user is logged in or not. You can use these query methods to check that the elements are rendered.

//UserInfo.tsx
function UserInfo({ isLoggedIn, user}: {isLoggedIn:boolean, user:string}) {
    if (!isLoggedIn){
        return <button>Login</button>
    }
    return (
        <div>
          <p>Welcome, {user}!</p>
          <button>Logout</button>
        </div>
    ) 
};
export default UserInfo;

//UserInfo.test.tsx
import {it, expect, describe} from 'vitest';
import {render, screen } from '@testing-library/react';
import "@testing-library/jest-dom/vitest";
import UserInfo from './UserInfo';

describe('UserInfo component', () => {
    it('displays info for loggedin user', () => {
        render( <UserInfo isLoggedIn={true} user="user 1" />); //renders the element in the DOM
        let welcomeText = screen.getByText(/Welcome.*user/i ) //matches the element that contains 'Welcome' and 'user' text 
        let logoutButton = screen.getByRole('button', {name: /logout/i}); //matches a button element with a text content of 'logout' 

        expect(welcomeText).toBeInTheDocument(); 
        expect(logoutButton).toBeInTheDocument();
    })
})
Enter fullscreen mode Exit fullscreen mode

The variants are the same for getBy and queryBy methods. (getByRole does the same thing as queryByRole, getByText does the same thing as queryByText, and so on.). The difference them is that getBy returns an error if the element is not found while queryBy returns null if the element is not found.

For example, let’s say you want to test that the login button is not rendered when the user is logged in. If you use getBy to query the login button, you’ll get an error.

describe('UserInfo component', () => {
    it('does not display login button when user is logged in', () => {
        render( <UserInfo isLoggedIn={true} user="user 1" />);
        let loginButton = screen.getByRole('button', {name: /login/i});
        expect(loginButton).not.toBeInTheDocument();
    })
})
Enter fullscreen mode Exit fullscreen mode

Error message when using getBy

But when you use queryBy method, the test works.

describe('UserInfo component', () => {
    it('does not display login button when user is logged in', () => {
        render( <UserInfo isLoggedIn={true} user="user 1" />);
        let loginButton = screen.queryByRole('button', {name: /login/i});
        expect(loginButton).not.toBeInTheDocument(); //passes if the element is not rendered in the DOM
  })
})
Enter fullscreen mode Exit fullscreen mode

toBeInTheDocument()

The toBeInTheDocument() method is one of the matchers from jest-dom. It checks if the element is in the DOM. Other jest-dom matchers include toBeDisabled(), toBeChecked(), toHaveClass(), toHaveTextContent(), and so on.

import {it, expect, describe} from 'vitest';
import {render, screen } from '@testing-library/react';
import "@testing-library/jest-dom/vitest";
import Component from './Component';

describe('Component', () => {
    it('renders heading', () => {
        render( <Component />);
        let heading = screen.getByRole('heading') 
        expect(heading).toBeInTheDocument();
    })
})
Enter fullscreen mode Exit fullscreen mode

fireEvent object

The fireEvent object is used to simulate user interactions.

To use fireEvent, you first need to import it from the React testing library and then simulate whatever action you want. fireEvent supports events like click, change, submit, keypress, etc.

Let’s say, for example, you have a component that contains a form that displays the result entered in the input when submitted.

//FormComponent.tsx
import { useState } from 'react';

function FormComponent() {
  const [inputValue, setInputValue] = useState<string>('');
  const [submittedValue, setSubmittedValue] = useState<string | null>(null);
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // Prevent form reload
    if (inputValue.length > 0) {
      setSubmittedValue(inputValue);
      setInputValue('');
    } else {
      setSubmittedValue(null);
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Enter some info"
        />
        <button type="submit" data-testid="submit-button">Submit</button>
      </form>
      {submittedValue && <p>{submittedValue}</p>}
    </div>
  );
};
export default FormComponent;

//FormComponent.test.tsx
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import FormComponent from './FormComponent';

describe('FormComponent', () => {
  let inputElement: HTMLInputElement;
  let submitButton: HTMLButtonElement;

  beforeEach(() => {
    // Render the component once before each test
    render(<FormComponent />);
    // Setup references to elements
    inputElement = screen.getByPlaceholderText('Enter some info');
    submitButton = screen.getByTestId('submit-button');
  });

  afterEach(() => {
    // Cleanup the DOM after each test
    cleanup();
  });

  it('should render input content when form is submitted', () => {
    // Simulate entering text in the input
    fireEvent.change(inputElement, { target: { value: 'Test input' } 
  });

    // Simulate clicking the submit button
    fireEvent.click(submitButton);
    // Assert that the paragraph with submitted text is displayed
    expect(screen.getByText('Test input')).toBeInTheDocument();
  });

  it('should not render anything if the input is empty on submit', () => {
    // Simulate clicking the submit button without entering any text
    fireEvent.click(submitButton);
    // Assert that the paragraph is not rendered
    const paragraph = screen.queryByText('Test input');
    expect(paragraph).not.toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

What can I test when using Vitest and React-testing-library?

When testing React components, you’re to test the output not the implementation. You can test how the component renders when given specific props or the element that renders when a button is clicked, and so on.

What are the examples of tests in can run in React apps?

There are different scenarios for tests you can write for your React apps.

In a todo app, you can test if the list renders correctly when a new note is added.

In an e-commerce site, you can test if the product information is correctly rendered in the cart component when the 'add to cart' button is clicked.

For forms, you can test for password strength, invalid inputs, and form submission.

How do I run an end-to-end test with Vitest?

Vitest is best suited for unit and integration testing. For end-to-end testing, tools like Cypress, Playwright, or Puppeteer are better suited for it.

How do I test components with state?

If you have a component that depends on some internal states. You can test that the UI updates correctly in response to the state changes.

Let’s say, for example, you have an 'add to cart' and 'remove from cart' button. You can test that the 'add to cart' increases the total product and 'remove from cart' removes the total product.

Is it necessary to test React state and hooks?

No, it isn’t. You should run tests based on how the user sees and interacts with it. The user doesn’t see state or hooks since they are internal implementations. So should only test that the content displays as it should.

Can I run the npm test and npm run dev commands together?

Yes, you can.

You can do this by using either the Concurrently library or runnint them in separate terminal windows

Concurrently

The first method is by using a library like Concurrently to run the 2 scripts together.

To set this up, you first need to install concurrently as a development dependency

npm install --save-dev concurrently
Enter fullscreen mode Exit fullscreen mode

After that, you should create a new script in the package.json file that concurrently runs the two scripts.

"scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui",
    "start:all": "concurrently \"npm run dev\" \"npm run test\""
}
Enter fullscreen mode Exit fullscreen mode

Then you can finally run npm run start:all in your CLI

Separate terminal windows

The second method is by creating 2 terminal windows. One for running the React code. The other for running Vitest in watch mode.

First, you need to open a terminal, navigate to the project’s directory, and run the command below to run your React code.

npm run dev
Enter fullscreen mode Exit fullscreen mode

Next, you should open another terminal window, navigate to the project directory and run Vitest in watch mode.

npm run test:watch
Enter fullscreen mode Exit fullscreen mode

How do I skip tests in a file?

You can use the skip() method to temporarily exclude tests you don’t want to run. You can skip individual tests or an entire test suite.

Here is the syntax:

describe('test-suite', (){
  it.skip('test case 1', () => { //this test case is skipped
    //assertions
  })

 it.skip('test case 2', () => { //this test case is also skipped
    //assertions
  })

  it('test case 3', () => { //this test is executed
    //assertions
  })
})
Enter fullscreen mode Exit fullscreen mode

What are the common configurations in the vitest config file?

The properties in the vite.config.ts (or vitest.config.ts) file allow you to control how the test is run. Every property for configuring the test must be within the test object.

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

export default defineConfig({
  plugins: [react()],
  test: {
    //properties
  }
});
Enter fullscreen mode Exit fullscreen mode

The common properties include environment, globals, setupFiles, and so on.

environment

The environment property specifies the environment in which the test will run. If you’re using Vitest to test your React components, the environment needs to simulate a browser environment. This means the value can be either jsdom or happy-dom. However, if you’re running the test for Node.js, the value should be node.

test: {
    environment: 'jsdom'
}
Enter fullscreen mode Exit fullscreen mode

globals

When globals is set to true, you’ll be able to use global functions(like describe, it, expect, etc) from Vitest without explicitly importing it.

test: {
    environment: 'jsdom',
    globals: true
}
Enter fullscreen mode Exit fullscreen mode

setupFiles

setupFiles defines the path to files that’ll be executed before each test files. Adding property also reduces the number of imports you add in your test files.

test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: `./src/setup.ts`
}
Enter fullscreen mode Exit fullscreen mode

In the setup.ts file, you can store all the repetitive imports in it.

import {render, screen} from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
Enter fullscreen mode Exit fullscreen mode

Once that is done, the only import you’ll need will be the component you want to test.

//File.test.tsx
import Component from ./component;

describe('test suite', (){
  it('test case 1', () => {
    //assertions
  }
  it('test case 2', () => {
    //assertions
  }
})
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we covered a lot of things. You learned how to set up and test React code in Vitest. You also got answers to some of the questions you might have asked (or thought) about when running tests with Vitest. I hope you find this article a useful guide to learning how to run React tests with Vitest. Thanks for reading. Bye.

References

. . .
Terabox Video Player