Diego Betto
Photo by Nguyen Dang Hoang Nhu on Unsplash

Diego Betto · September 22, 2026 · 4 min di lettura

Vitest: what it is, how to install it, and how to use it with React 19

A practical guide to Vitest: installation, configuration, testing React 19 components (including useActionState), mocking with vi, coverage, and a comparison with Jest and node:test.

Condividi:XLinkedInFacebookWhatsApp

Vitest is the test runner built by the Vite team, designed to share the exact same transform pipeline as the bundler: the same vite.config.ts, the same plugins, the same aliases, no parallel configuration to maintain. The API is deliberately compatible with Jest’s (describe, it, expect), so migrating from an existing Jest project is almost always a change of imports, not a rewrite.

Installation

npm install -D vitest

For React projects you also need a DOM environment and testing utilities:

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

Configuration goes in the same file as Vite’s (or a separate vitest.config.ts, if you’d rather keep it isolated from the build config):

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

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: "./vitest.setup.ts",
  },
});
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";

globals: true avoids having to import describe/it/expect in every file — the behavior anyone coming from Jest already expects.

A first test

// sum.test.js
import { expect, test } from "vitest";
import { sum } from "./sum.js";

test("adds two positive numbers", () => {
  expect(sum(2, 3)).toBe(5);
});
npx vitest

By default it starts in watch mode and only reruns the tests touched by changed files, using the same dependency graph Vite uses for HMR — this is the most noticeable practical difference from Jest: there’s no separate transform to recompute, already-transformed files stay cached.

Testing a React 19 component

Example using useActionState, the hook introduced in React 19 for managing state derived from a form action:

// LikeButton.tsx
import { useActionState } from "react";

async function likeAction(prevState: number) {
  await new Promise((resolve) => setTimeout(resolve, 50));
  return prevState + 1;
}

export function LikeButton() {
  const [likes, formAction, isPending] = useActionState(likeAction, 0);

  return (
    <form action={formAction}>
      <button type="submit" disabled={isPending}>
        {isPending ? "..." : `Like (${likes})`}
      </button>
    </form>
  );
}
// LikeButton.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test } from "vitest";
import { LikeButton } from "./LikeButton";

test("increments likes after submit", async () => {
  const user = userEvent.setup();
  render(<LikeButton />);

  await user.click(screen.getByRole("button"));

  expect(await screen.findByText("Like (1)")).toBeInTheDocument();
});

findByText is necessary because formAction is asynchronous: the button first goes through the isPending state, and you need to wait for the final re-render instead of reading the DOM immediately with getByText. For more on React 19’s other hook-system changes (including use()), see the article on use vs useContext.

Mocking with vi

import { vi, test, expect } from "vitest";
import { registerUser } from "./registerUser.js";

test("calls the email service with the right arguments", () => {
  const sendEmail = vi.fn(() => true);

  registerUser({ email: "[email protected]" }, { sendEmail });

  expect(sendEmail).toHaveBeenCalledWith("[email protected]");
});

vi.spyOn() temporarily replaces a method on an existing object, vi.mock() replaces an entire module (handy for isolating fetch calls or HTTP clients) — the same conceptual surface as jest.fn/jest.spyOn/jest.mock, just different names.

💡 Consiglio

npx vitest --ui opens a browser dashboard listing tests, results, and a timeline of individual assertions — useful for debugging a large suite without reading terminal output. It requires the separate @vitest/ui package.

Coverage

npm install -D @vitest/coverage-v8
npx vitest run --coverage

Coverage uses the same V8 instrumentation as Node, with a text report in the console and a browsable HTML report generated in coverage/ — ready to use, no need to configure istanbul/nyc separately.

Vitest vs Jest vs node:test

Vitest Jest node:test
Shares config with the build Yes (same vite.config.ts) No (separate config) N/A
TypeScript/JSX with no extra setup Yes Needs ts-jest/babel-jest Needs an external loader
DOM environment (jsdom) Yes, with a flag Yes, with a flag Manual
Snapshot testing Yes Yes No
Dependencies Vite + any plugins None extra (but the Babel ecosystem is often already there) Zero
Watch mode based on Vite’s dependency graph (HMR) File system polling File system

Jest remains the more mature choice for projects not built on Vite, with years of plugins and editor extensions behind it. node:test is the zero-cost option for libraries with no DOM dependencies, but it lacks snapshot testing and a built-in jsdom environment — I compared it in more detail in the article on node:test vs Jest. Vitest sits in the middle: for projects already on Vite, Astro, Next.js (in Vite mode), or any modern React/TypeScript setup, it’s almost always the lowest-friction choice, since it reuses configuration and bundler speed the project already has.

When it’s worth choosing

If the project is already on Vite (or a Vite-based framework), Vitest avoids maintaining two separate build pipelines and starts faster thanks to the shared transform. For legacy Webpack projects with a large, stable Jest suite, migrating rarely pays for the time it costs — unless slow CI test runs are already a concrete problem to solve.

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.