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: falseand importdescribe,test, andexpectexplicitly. 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:
| Matcher | Purpose |
|---|---|
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 / rejects | Unwrap 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:
| Factor | Vitest | Jest |
|---|---|---|
| ESM/TS | Native via Vite | Needs Babel / ts-jest transforms |
| Transform cache | Shared Vite cache, on-demand | Per-file transform |
| Watch granularity | Module-graph aware | Heuristic file matching |
| Config | Reuses vite.config | Separate jest.config |
Warning: Vitest and Jest globals are similar but not identical.
jest.fn()becomesvi.fn(), and timer helpers live on theviobject (vi.useFakeTimers()). Update imports when migrating a Jest suite.
Best practices
- Keep
globals: falseand import test helpers explicitly for cleaner scope and better linting. - Set
test.environmentto"node"for backend code so browser APIs are not mocked in needlessly. - Prefer
await expect(promise).resolves/rejectsover manualtry/catchfor async assertions. - Use
vi.fn()andvi.spyOn()for mocks, and reset them inafterEachwithvi.restoreAllMocks(). - Run
vitestin watch mode locally andvitest runin CI to avoid hanging pipelines. - Co-locate
*.test.jsfiles next to the code they cover so the module graph keeps watch reruns tight.