Test Runner

Vitest

Vitest is the fast, Vite-native test runner. It reuses your build config, runs tests in parallel and ships Jest-compatible APIs with first-class TypeScript support.

intermediate13 min readUpdated Sep 15, 2026
sum.test.ts
ts
// sum.test.ts
import { describe, it, expect } from "vitest";
import { sum } from "./sum";

describe("sum", () => {
  it("adds two numbers", () => {
    expect(sum(2, 3)).toBe(5);
  });

  it("handles negatives", () => {
    expect(sum(-1, -1)).toBe(-2);
  });
});
Powered by
Vite
API
Jest-compatible
Language
TypeScript native
Mocks
vi.fn, vi.mock
Watch mode
Instant, Vite HMR
Browser mode
Real browser testing

Why it matters

Why Vitest is winning

Fast by default

Tests run through Vite's transform pipeline in parallel workers, so feedback is nearly instant even in large suites.

Reuses your config

Aliases, plugins and TypeScript settings come from the same Vite config your app already uses, so there is nothing to duplicate.

Jest-compatible API

describe, it, expect and the mocking helpers mirror Jest, which makes migration and shared knowledge straightforward.

The big picture

The three ideas behind Vitest

A Vite-native pipeline, a Jest-compatible API and parallel workers that keep the feedback loop fast.

The runner

Execute

Finds test files, runs them in parallel workers and reports results with a clear, fast UI.

The transform

Build

Uses Vite to compile TypeScript, JSX and aliases exactly as your app does.

Assertions and mocks

Verify

expect, vi.fn and vi.mock cover assertions, spies and module mocking.

Vitest at a glance

The core of Vitest

describe and it

Group related tests and name each one by the behaviour it verifies.

expect

A rich matcher API for values, objects, errors and async results.

vi.fn and vi.spyOn

Create mocks and spies to isolate the code under test.

vi.mock

Replace an entire module with a mock, hoisted above imports.

Async testing

Await promises and use expect.resolves or expect.rejects.

Coverage and UI

Built-in coverage reporting and an optional browser UI.

A short history

A test runner built for Vite

  1. 2021

    Vitest announced

    Anthony Fu introduces a Vite-native test runner that reuses the existing pipeline.

    21
  2. 2022

    Vitest 1.0 groundwork

    Rapid releases add coverage, browser mode and Jest compatibility.

    22
  3. 2024

    Vitest 1 and 2

    A stable API, workspace projects and browser mode mature.

    24
  4. Today

    The default for Vite

    The recommended test runner for Vite, Vue, Svelte, Solid and many React setups.

    Today

The complete guide

Vitest: Everything you need to know

What is Vitest?

Vitest is a test runner powered by Vite. It reuses your existing Vite configuration — aliases, plugins, TypeScript and JSX transforms — so tests run through the same pipeline as your application. That shared foundation is why it starts fast and stays fast.

The API will feel familiar if you have used Jest. describe, it, expect and the mocking helpers all work the same way. What changes is the engine underneath: Vitest runs tests in parallel workers, watches files with Vite’s module graph and ships first-class TypeScript support without extra configuration.

Writing your first test

A test file imports from vitest and describes the behaviour it verifies.

// sum.test.ts
import { describe, it, expect } from "vitest";
import { sum } from "./sum";

describe("sum", () => {
  it("adds two numbers", () => {
    expect(sum(2, 3)).toBe(5);
  });

  it("handles negatives", () => {
    expect(sum(-1, -1)).toBe(-2);
  });
});

Name tests after the behaviour, not the function. A failing test that says “adds two numbers” tells you what broke; one that says “sum test 1” does not. Group related cases with describe and use beforeEach and afterEach for setup and cleanup.

Assertions

Vitest ships a rich matcher API, close to Jest’s.

// assertions.test.ts
expect(value).toBe(5);              // strict equality
expect(value).toEqual({ a: 1 });    // deep equality
expect(value).toBeTruthy();
expect(list).toHaveLength(3);
expect(list).toContain("a");
expect(fn).toThrow("invalid");
expect(value).toBeGreaterThan(3);

Use toEqual for objects and arrays and toBe for primitives and reference checks. toMatchObject and toContainEqual are useful when you only care about part of a structure.

Mocks and spies

Isolation is central to unit testing. Vitest provides vi.fn for mocks, vi.spyOn for wrapping existing methods and vi.mock for replacing whole modules.

// api.test.ts
import { vi, describe, it, expect } from "vitest";
import { fetchUser } from "./api";

vi.mock("./http", () => ({
  get: vi.fn().mockResolvedValue({ id: 1, name: "Ada" }),
}));

describe("fetchUser", () => {
  it("returns the user", async () => {
    await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: "Ada" });
  });
});

vi.mock is hoisted above imports, so the mock is registered before the module under test loads it. When the factory needs a variable defined in the test file, use vi.hoisted so the value exists before the mock runs.

Async testing

Await the operation and assert on the result. Vitest provides resolves and rejects helpers for promises.

// async.test.ts
it("rejects on failure", async () => {
  await expect(loadUser(-1)).rejects.toThrow("Invalid id");
});

Prefer this style over the done callback. Forgetting done causes timeouts that are hard to diagnose, and unhandled rejections can leak between tests. For fake timers, vi.useFakeTimers() and vi.advanceTimersByTime() let you test debounces and intervals deterministically.

Snapshots

Snapshots capture a value and compare it on later runs. They are useful for large, stable output such as serialised data or rendered markup, but they are easy to abuse.

// snapshot.test.ts
it("matches the shape", () => {
  expect(createConfig({ debug: true })).toMatchSnapshot();
});

A snapshot that is updated without reading it is worse than no test. Use them for data structures where a full comparison is impractical, and prefer explicit assertions when you know what matters. toMatchInlineSnapshot keeps the expected value in the test file, which makes reviews meaningful.

Configuration

Vitest reads a test block from your Vite config or a dedicated vitest.config.ts.

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    globals: true,
    coverage: {
      provider: "v8",
      reporter: ["text", "html"],
      thresholds: { lines: 80 },
    },
  },
});

The environment option selects node, jsdom, happy-dom or a browser. For component tests, jsdom or happy-dom is typical; for pure logic, keep the fast node environment. Coverage thresholds turn a coverage goal into an enforced rule.

Vitest and the testing pyramid

Vitest is the unit and integration layer. Pair it with a component-testing library such as Testing Library for UI behaviour, and with an end-to-end tool like Playwright for full user flows. Together they cover the pyramid: many fast unit tests, fewer integration tests and a handful of end-to-end tests.

Best practices

  • Test behaviour, not implementation details.
  • Keep each test focused on one behaviour.
  • Use beforeEach for shared setup and clean up mocks with vi.clearAllMocks.
  • Prefer resolves and rejects over done callbacks.
  • Mock at the boundary (network, time, randomness), not every internal call.
  • Use coverage thresholds to prevent silent regressions.
  • Run in watch mode while developing and in CI on every push.

Common mistakes

  • Asserting on internal state or private methods instead of observable behaviour.
  • Updating snapshots without reviewing the diff.
  • Forgetting to clear mocks between tests and leaking state.
  • Using real timers and making tests slow and flaky.
  • Over-mocking until the test only verifies the mocks.
  • Testing the same behaviour at every level of the pyramid.

Where to go next

Vitest is the fastest way to start testing a Vite project. Compare it with the classic Jest, add Testing Library for components and Playwright for end-to-end flows, and lean on Vite to keep the configuration shared. Then write a few tests for code you already have and watch a refactor become safe.

Testing async code

Await the promise and assert on the result. The done callback is easy to forget and produces confusing timeouts.

Prefer
it("loads the user", async () => {
  await expect(getUser(1)).resolves.toEqual({
    id: 1,
    name: "Ada",
  });
});
Avoid
it("loads the user", (done) => {
  getUser(1).then((user) => {
    expect(user.name).toBe("Ada");
    done();
  });
});

Mocking a module

vi.mock is hoisted above imports, so the module is replaced before the code under test loads it.

Prefer
import { vi } from "vitest";

vi.mock("./api", () => ({
  fetchUser: vi.fn().mockResolvedValue({
    id: 1,
  }),
}));
Avoid
import * as api from "./api";
// reassigning after import
// is brittle and often fails
api.fetchUser = async () => ({ id: 1 });

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Vitest?

Our interactive tutorial walks you through Vitest step by step — with quizzes and real code you can run in the browser.