A glance at Vitest

I’ve recently had the chance to work on front-end testing for a short time, and I wanted to write this note for anyone who’s new to Vitest, curious about the current state of front-end testing, or just checking out my writing skills (please do not roast me!).

A Quick Introduction

We have heard about Jest for a while—it's widely used, backed by Meta, and is the default option when you first initialize a React project.

But now we have Vitest. What is it, why should we use it, and how do we set it up?

Why Vitest?

Once Upon a Time

Before browsers could support ES Modules natively, bundlers like Webpack with Babel or other tools were needed to transform ES Modules (ESM) into a single CommonJS format file. This approach provided cleaner code, correct import sequences, and fewer network requests, making it executable in older browsers.

For example, when you write this code:

useToggle.tsx
1import useToggle from "./useToggle";
2
3// ... your code ...
4
5export { foo };

Babel jumps in and transforms it to CommonJS. Here's what it does:

From TSX:

  • Remove types
  • Transpile JSX
  • Transpile ESM

To CommonJS: (so older browser versions that don't support ESM can execute it)

It transforms to:

useToggle.js
1const useToggle = require("./useToggle");
2
3// ... your code ...
4
5module.exports = { foo };

At that time, when the Meta team built ReactJS, they needed a test runner. Following this pattern, the team built Jest, which takes CommonJS as input for testing.

And Then...

Browsers now support ES Modules directly. When you import anything, the browser understands it, picks up the file, and if that file contains any import statements, it can import them dynamically without any concerns.

With Vite, since browsers support ES Modules directly, Vite takes advantage of this and doesn't need to compile to CommonJS. It serves ESM directly, eliminating the bundler step and enabling faster development builds. You can understand the flow as:

TSX

  • Remove types
  • Transform JSX

To ESM: (which modern browsers that support ES Modules can execute)

The problem now is that if you use Vite with Jest, you need two different compilation strategies:

  • Development: TSX → (esbuild) → ESM
  • Test: TSX → (Babel) → CommonJS

You have two different pipelines for the same job—why not use just one? That's why we have Vitest - Vite-native test runner. Vitest can understand ESM directly, so we have consistent behavior. And yes, Vitest can understand both ESM and CommonJS, so Vitest can be used even with non-Vite projects.

Vitest is the default test option when you initialize a project with Vite, but what if you're not using Vite?

Setting Up Vitest Without Vite

Vitest

Let's add our main character: Vitest.

terminal
1yarn add -D vitest

Then, create a vitest.config.ts file in the root of your project:

vitest.config.ts
1import { defineConfig } from "vitest/config";
2
3export default defineConfig({
4 test: {
5 globals: true,
6 },
7});

Since we are testing React, we need to add a plugin for Vitest to understand React components:

terminal
1yarn add -D @vitejs/plugin-react

Then, add the plugin to your vitest.config.ts file:

vitest.config.ts
1import { defineConfig } from "vitest/config";
2import react from "@vitejs/plugin-react";
3
4export default defineConfig({
5 plugins: [react()],
6 test: {
7 globals: true,
8 environment: "jsdom",
9 include: ["src/**/*.test.{js,jsx,ts,tsx}"],
10 },
11});

Now, let's modify our package.json file. To get beautiful coverage reports, let's add @vitest/coverage-v8 to our dev dependencies:

terminal
1yarn add -D @vitest/coverage-v8

And add a script to your package.json file:

package.json
1"scripts": {
2 "test": "vitest --coverage"
3}

At this point, you can run your tests with yarn test. However, if you initialize the project from the beginning, when you run yarn test with the initial project, it will fail with the default test. The error looks like this:

error
1Invalid Chai property: toBeInTheDocument

Reference: https://stackoverflow.com/questions/77611978/invalid-chai-property-in-vitest

If you are seeing this, we're missing something (else you can skip RTL section below or checking what is going on with others)...

React Testing Library

Let's add @testing-library/react and @testing-library/jest-dom to our project (if you don't have them yet, but you should have them if you initialize the project from the beginning):

terminal
1yarn add -D @testing-library/react

And @testing-library/jest-dom:

terminal
1yarn add -D @testing-library/jest-dom

Let's put it inside our setup file setupTests.ts:

setupTests.ts
1import "@testing-library/jest-dom";

Now, let's create the setup file and enhance it a bit more:

setupTests.ts
1import "@testing-library/jest-dom";
2import { cleanup } from "@testing-library/react";
3import { afterEach } from "vitest";
4
5afterEach(() => {
6cleanup();
7});

This is for cleanup after each test.

And let's add it to our vitest.config.ts file:

vitest.config.ts
1import { defineConfig } from "vitest/config";
2import react from "@vitejs/plugin-react";
3
4export default defineConfig({
5 plugins: [react()],
6 test: {
7 globals: true,
8 environment: "jsdom",
9 include: ["src/**/*.test.{js,jsx,ts,tsx}"],
10 setupFiles: ["./src/setupTests.ts"],
11 },
12});

That's all for now

Now when you run yarn test, the result will be seen at terminal as (for example):

terminal
1✓ src/components/CustomHook/useToggle.test.tsx (2 tests) 2075ms
2 ✓ useToggle > should toggle the state without action 1020ms
3 ✓ useToggle > should toggle the state with action 1040ms
4
5Test Files 7 passed (7)
6Tests 17 passed (17)
7Start at 16:52:52
8Duration 4.19s (transform 295ms, setup 553ms, collect 1.44s, tests 3.42s, environment 7.29s, prepare 532ms)

Features

Like other test runners, Vitest supplies so many matchers that support the assertion. But let's talk about other features too:

UI mode

This is pretty cool feature, let's see:

ui mode

On the left hand is the test suite, with the icon of result. The main part is the Module Graph, where you can see the dependency graph of the testing files. Ignoring node_modules supports cleaner view.

If you want to try, add --ui flag at the end of your test command:

package.json
1"scripts": {
2 "test": "vitest --ui"
3}

It will automatically opens a tab for you, but if not, it's here:

url
1http://localhost:51204/__vitest__/#/

Watch mode

Vitest turn on this mode by default, and only triggers the related test files instead of everything, which are affected by the changed, or the file you are running and the related (as your imports) specifically, and excludes the rest. This deliver faster and more focus develop experience.

Small note, the affected one is opposite with the one at Module Graph, that one is what the component needs, this one are what needs the component.

Test coverage

This is the basic feature that all test runner should have, I do agree. With Vitest, instead of logging out all of the test results, even the unrelated files, they only displays the one which is run at watch mode.

I give you the sneak peak at the implementation, I added --coverage to the script above:

package.json
1"scripts": {
2 "test": "vitest --coverage"
3}

In-source testing

Reading at the docs here, I tried, you can just take a quick look:

utils.ts
1export const convertBytes = (bytes: number) => {
2 if (bytes < 1024) {
3 return `${bytes} bytes`;
4 }
5 if (bytes < 1024 * 1024) {
6 return `${(bytes / 1024).toFixed(2)} KB`;
7 }
8 return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
9};
10
11if (import.meta.vitest) {
12const { it, expect } = import.meta.vitest;
13it("convertBytes", () => {
14expect(convertBytes(1)).toBe("1 bytes");
15expect(convertBytes(1024)).toBe("1.00 KB");
16expect(convertBytes(1024 * 1024)).toBe("1.00 MB");
17});
18}

They include the test into the code file, without extracting to seperated test file. This can be good, when you share the same closure, but my insight is this only applicable for the util, helper which is pure logic, no complicated effects, dom, ... But this is a new way we can consider for writing test.

And so many more to be added.

That's all! Have fun with testing!

Conclusion

I have a good testing experience with Vitest, easy to config, clean coverage output, fast and focus execution, easy to debug my test, so, putting my trust in Vitest, for now.

Reference

Vitest docs

Jest docs

Table of Contents

A Quick Introduction
Why Vitest?
Once Upon a Time
And Then...
Setting Up Vitest Without Vite
Vitest
React Testing Library
That's all for now
Features
UI mode
Watch mode
Test coverage
In-source testing
Conclusion
Reference