# 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.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-09-22
- URL: https://diegobetto.com/en/vitest-guide-react-19/
- Tag: testing, vitest, react, javascript

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

```bash
npm install -D vitest
```

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

```bash
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):

```ts
// 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",
  },
});
```

```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

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

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

```bash
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:

```tsx
// 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>
  );
}
```

```tsx
// 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`](https://diegobetto.com/en/react-use-vs-usecontext/).

### Mocking with `vi`

```js
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: "diego@example.com" }, { sendEmail });

  expect(sendEmail).toHaveBeenCalledWith("diego@example.com");
});
```

`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.

> `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

```bash
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](https://diegobetto.com/en/node-test-runner-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.
