Skip to content
Node.js nd testing 4 min read

Testing with Vitest

Vitest is a modern test framework built on top of Vite’s transform pipeline. It offers a Jest-compatible API (describe, test, expect) while delivering native ESM and TypeScript support out of the box, an instant watch mode, and noticeably faster startup than Jest. If you are already using Vite for a frontend build—or simply want a zero-config, ESM-first runner for Node.js—Vitest is the path of least resistance.

Installing and configuring

Vitest is a dev dependency. Install it alongside an assertion-free project and you are ready to write tests immediately.

npm install --save-dev vitest

Add scripts to package.json. By default vitest runs in watch mode in a TTY and runs once in CI; vitest run forces a single run.

{
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  }
}

Configuration is optional. When you need it, Vitest reuses your vite.config.ts or accepts a dedicated vitest.config.ts. The test.environment option selects the runtime—use node for backend code.

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

export default defineConfig({
  test: {
    environment: "node",
    globals: false, // explicit imports keep things tidy
    include: ["**/*.test.js"],
  },
});

Tip: keep globals: false and import describe, test, and expect explicitly. This avoids polluting the global scope and makes your tests easier to type-check and lint.

Writing your first test

The API mirrors Jest. A test file groups related cases with describe, declares cases with test (or its alias it), and asserts with expect. Suppose we have a small module to test.

// math.js
export function add(a, b) {
  return a + b;
}

export async function fetchTotal(items) {
  return items.reduce((sum, n) => sum + n, 0);
}
// math.test.js
import { describe, test, expect } from "vitest";
import { add, fetchTotal } from "./math.js";

describe("math utilities", () => {
  test("adds two numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  test("sums an array asynchronously", async () => {
    await expect(fetchTotal([1, 2, 3])).resolves.toBe(6);
  });
});

Run the suite once:

npx vitest run

Output:

 ✓ math.test.js (2 tests) 3ms
   ✓ math utilities > adds two numbers
   ✓ math utilities > sums an array asynchronously

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  10:42:11
   Duration  214ms

The expect API

Vitest ships a rich matcher set that is largely a drop-in replacement for Jest. A few of the most common matchers:

MatcherPurpose
toBe(value)Strict Object.is equality (primitives, references)
toEqual(value)Deep structural equality
toStrictEqual(value)Deep equality including undefined keys and types
toContain(item)Array/string membership
toThrow(error?)Asserts a function throws
resolves / rejectsUnwrap a promise before asserting
toHaveBeenCalledWith(...)Inspect mock/spy calls
import { test, expect } from "vitest";

test("matcher sampler", () => {
  expect({ id: 1, name: "Ada" }).toEqual({ id: 1, name: "Ada" });
  expect([1, 2, 3]).toContain(2);
  expect(() => JSON.parse("{")).toThrow(SyntaxError);
});

Setup, teardown, and lifecycle hooks

Use lifecycle hooks to prepare and clean up shared state. beforeEach/afterEach run around every test; beforeAll/afterAll run once per file.

import { describe, test, expect, beforeEach, afterEach } from "vitest";

describe("counter", () => {
  let count;

  beforeEach(() => {
    count = 0;
  });

  afterEach(() => {
    count = null;
  });

  test("increments", () => {
    count += 1;
    expect(count).toBe(1);
  });
});

Native ESM and TypeScript

Because Vitest sits on Vite’s transform pipeline, it understands ES modules and TypeScript natively—no Babel config, no ts-jest, and no --experimental-vm-modules flag. You can import .ts files directly and use top-level await in test files. CommonJS still works too: if your project omits "type": "module", require()-based modules are handled transparently, and you can mix both styles during a migration.

// user.test.ts
import { test, expect } from "vitest";
import { createUser } from "./user.ts";

test("creates a user with defaults", () => {
  const user = createUser({ name: "Grace" });
  expect(user.role).toBe("member");
});

Watch mode and why Vitest is fast

Running vitest with no arguments starts watch mode. It uses Vite’s module graph to re-run only the tests affected by a changed file, giving near-instant feedback.

npx vitest

Output:

 ✓ math.test.js (2 tests) 3ms

 Test Files  1 passed (1)
      Tests  2 passed (2)

 PASS  Waiting for file changes...
       press h to show help, press q to quit

Vitest’s speed advantage over Jest comes from a few design choices:

FactorVitestJest
ESM/TSNative via ViteNeeds Babel / ts-jest transforms
Transform cacheShared Vite cache, on-demandPer-file transform
Watch granularityModule-graph awareHeuristic file matching
ConfigReuses vite.configSeparate jest.config

Warning: Vitest and Jest globals are similar but not identical. jest.fn() becomes vi.fn(), and timer helpers live on the vi object (vi.useFakeTimers()). Update imports when migrating a Jest suite.

Best practices

  • Keep globals: false and import test helpers explicitly for cleaner scope and better linting.
  • Set test.environment to "node" for backend code so browser APIs are not mocked in needlessly.
  • Prefer await expect(promise).resolves/rejects over manual try/catch for async assertions.
  • Use vi.fn() and vi.spyOn() for mocks, and reset them in afterEach with vi.restoreAllMocks().
  • Run vitest in watch mode locally and vitest run in CI to avoid hanging pipelines.
  • Co-locate *.test.js files next to the code they cover so the module graph keeps watch reruns tight.
Last updated June 14, 2026
Was this helpful?