Vitest Complete Guide: Setup, Unit Testing, Mocking, and Coverage in TypeScript
The complete Vitest guide for TypeScript projects. Learn how to set up Vitest, write unit and integration tests, use mocks and spies, configure coverage, and integrate with React and Node.js — with 10+ practical examples.
Vitest is a Vite-native testing framework that has rapidly become the default choice for modern JavaScript projects. It uses the same configuration as Vite, supports TypeScript and JSX out of the box, runs tests in parallel using worker threads, and delivers a Jest-compatible API — meaning most Jest tests run in Vitest without changes.
This guide covers everything you need to go from zero to a fully configured test suite: installation, configuration, writing tests, mocking dependencies, testing React components, generating coverage reports, and common patterns for real-world projects.
TL;DR
- Install with
npm install -D vitest— zero configuration for Vite projects - Test files:
*.test.ts,*.spec.ts, or files inside__tests__/ - API mirrors Jest:
describe,it,expect,beforeEach,afterEach,vi.fn(),vi.mock() - Use
vi.mock()for module mocking,vi.spyOn()for spies - Coverage via
@vitest/coverage-v8or@vitest/coverage-istanbul vitest --uigives you an interactive browser-based test runner- In-source testing with
import.meta.vitestlets you colocate tests with implementation
Why Vitest in 2026
The JavaScript testing landscape has Jest as the incumbent — battle-tested, with a massive ecosystem. Vitest’s advantages are practical:
Speed: Vitest uses Vite’s transform pipeline and worker threads. Cold start is typically under 500ms. Rerunning a changed file in watch mode is near-instant. Jest’s startup time (loading Babel, configuring jsdom) is measured in seconds.
Zero config for Vite projects: If you’re already using Vite, Vitest inherits your vite.config.ts — path aliases, plugins, and environment settings all just work.
Native TypeScript and ESM: No need for Babel or separate TypeScript preprocessing. Vitest handles .ts, .tsx, .mts, and dynamic import() natively.
Jest compatibility: Vitest’s API is a superset of Jest’s. Most existing test suites migrate by replacing jest.fn() with vi.fn() and updating the config file.
Installation
For Vite projects
npm install -D vitest
Add a test script to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
For non-Vite projects (Node.js, etc.)
npm install -D vitest vite
Create vitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // enables describe/it/expect without imports
environment: "node", // or "jsdom" for browser-like environment
include: ["**/*.{test,spec}.{ts,tsx,js,jsx}"],
exclude: ["node_modules", "dist"],
},
});
For React projects
npm install -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
},
});
// src/test/setup.ts
import "@testing-library/jest-dom";
Writing Your First Tests
Vitest’s test API mirrors Jest exactly. If you know Jest, you already know Vitest.
Basic Unit Test
// src/utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// src/utils/math.test.ts
import { describe, it, expect } from "vitest";
import { add, divide } from "./math";
describe("add", () => {
it("adds two positive numbers", () => {
expect(add(2, 3)).toBe(5);
});
it("handles negative numbers", () => {
expect(add(-1, 1)).toBe(0);
});
});
describe("divide", () => {
it("divides two numbers", () => {
expect(divide(10, 2)).toBe(5);
});
it("throws on division by zero", () => {
expect(() => divide(10, 0)).toThrow("Division by zero");
});
});
Async Tests
Vitest handles Promises and async/await naturally:
// src/api/users.ts
export async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`User not found: ${id}`);
return response.json();
}
// src/api/users.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fetchUser } from "./users";
describe("fetchUser", () => {
beforeEach(() => {
// Reset mocks before each test
vi.restoreAllMocks();
});
it("returns user data on success", async () => {
const mockUser = { id: "1", name: "Alice" };
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => mockUser,
}));
const user = await fetchUser("1");
expect(user).toEqual(mockUser);
});
it("throws when user not found", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false }));
await expect(fetchUser("999")).rejects.toThrow("User not found: 999");
});
});
Mocking with vi
Vitest’s mock API is comprehensive. vi is the equivalent of Jest’s jest namespace.
vi.fn() — Mock Functions
import { vi, expect } from "vitest";
const mockCallback = vi.fn();
mockCallback("hello");
mockCallback("world");
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledWith("hello");
expect(mockCallback).toHaveBeenLastCalledWith("world");
// Return values
const mockAdd = vi.fn().mockReturnValue(42);
expect(mockAdd(1, 2)).toBe(42);
// Different return values per call
const mockSequence = vi.fn()
.mockReturnValueOnce("first")
.mockReturnValueOnce("second")
.mockReturnValue("rest");
vi.mock() — Module Mocking
vi.mock() replaces an entire module with a mock. It’s hoisted to the top of the file (like Jest’s jest.mock()):
// src/services/email.ts
export async function sendEmail(to: string, subject: string, body: string) {
// Real implementation uses SendGrid/SES
}
// src/services/user.test.ts
import { describe, it, expect, vi } from "vitest";
// vi.mock is hoisted — this runs before imports
vi.mock("./email", () => ({
sendEmail: vi.fn().mockResolvedValue({ success: true }),
}));
import { createUser } from "./user"; // createUser internally calls sendEmail
import { sendEmail } from "./email";
describe("createUser", () => {
it("sends a welcome email after creating user", async () => {
await createUser({ name: "Alice", email: "[email protected]" });
expect(sendEmail).toHaveBeenCalledWith(
"[email protected]",
"Welcome!",
expect.stringContaining("Alice")
);
});
});
vi.spyOn() — Spy on Real Implementations
Spies let you track calls without replacing the implementation (unless you want to):
import { vi, describe, it, expect, afterEach } from "vitest";
import * as fs from "node:fs";
describe("file operations", () => {
afterEach(() => {
vi.restoreAllMocks(); // restore original implementations
});
it("reads a config file", () => {
const spy = vi.spyOn(fs, "readFileSync").mockReturnValue('{"debug": true}');
const config = loadConfig("./config.json");
expect(spy).toHaveBeenCalledWith("./config.json", "utf-8");
expect(config.debug).toBe(true);
});
});
Mocking Timers
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
describe("retry logic", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("retries after delay", async () => {
const mockFn = vi.fn()
.mockRejectedValueOnce(new Error("timeout"))
.mockResolvedValue("success");
const promise = retryWithDelay(mockFn, 1000);
// Fast-forward time
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe("success");
expect(mockFn).toHaveBeenCalledTimes(2);
});
});
Testing React Components
With @testing-library/react and jsdom, you can test React components similarly to how you’d test in Jest + JSDOM:
// src/components/Counter.tsx
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setCount(c => c - 1)}>Decrement</button>
</div>
);
}
// src/components/Counter.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { Counter } from "./Counter";
describe("Counter", () => {
it("starts at zero", () => {
render(<Counter />);
expect(screen.getByTestId("count")).toHaveTextContent("Count: 0");
});
it("increments on button click", () => {
render(<Counter />);
fireEvent.click(screen.getByText("Increment"));
expect(screen.getByTestId("count")).toHaveTextContent("Count: 1");
});
it("decrements on button click", () => {
render(<Counter />);
fireEvent.click(screen.getByText("Decrement"));
expect(screen.getByTestId("count")).toHaveTextContent("Count: -1");
});
});
Testing Async Components
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { UserProfile } from "./UserProfile";
import * as api from "../api/users";
vi.mock("../api/users");
describe("UserProfile", () => {
it("displays user data after loading", async () => {
vi.mocked(api.fetchUser).mockResolvedValue({
id: "1",
name: "Alice",
email: "[email protected]",
});
render(<UserProfile userId="1" />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Alice")).toBeInTheDocument();
});
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});
});
Coverage Configuration
Install a coverage provider:
npm install -D @vitest/coverage-v8
Configure in vitest.config.ts:
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "json", "html"], // terminal, JSON for CI, HTML for browsing
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.d.ts", "src/test/**", "src/**/*.stories.*"],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
},
},
},
});
Run coverage:
npx vitest run --coverage
# Generates coverage/index.html — open in browser for interactive report
In-Source Testing
Vitest supports a unique pattern: writing tests directly in your source files. The test code is removed in production builds:
// src/utils/slug.ts
export function toSlug(str: string): string {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
// Tests live in the same file, removed in production
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it("converts string to slug", () => {
expect(toSlug("Hello World!")).toBe("hello-world");
expect(toSlug(" React + TypeScript ")).toBe("react-typescript");
});
}
Enable in config:
export default defineConfig({
test: {
includeSource: ["src/**/*.{ts,tsx}"],
},
define: {
"import.meta.vitest": "undefined", // removes tests in production builds
},
});
Snapshot Testing
Vitest supports both inline and file-based snapshots:
import { describe, it, expect } from "vitest";
import { formatDate } from "./date";
describe("formatDate", () => {
it("formats date correctly", () => {
// Creates/updates __snapshots__/date.test.ts.snap
expect(formatDate(new Date("2026-03-27"))).toMatchSnapshot();
// Inline snapshot — stored directly in the test file
expect(formatDate(new Date("2026-01-01"))).toMatchInlineSnapshot(
`"January 1, 2026"`
);
});
});
Update snapshots: vitest --update-snapshots or vitest -u
Vitest UI
Vitest ships with an optional browser-based test runner that shows test results, file trees, and test durations visually:
npm install -D @vitest/ui
npx vitest --ui
Opens at http://localhost:51204 by default. Useful for exploring a large test suite, seeing which tests are slow, and running individual tests interactively.
Common Configuration Patterns
Path Aliases (matching tsconfig.json)
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@test": path.resolve(__dirname, "./src/test"),
},
},
test: {
globals: true,
environment: "jsdom",
},
});
Multiple Environments in One Config
import { defineWorkspace } from "vitest/config";
export default defineWorkspace([
{
test: {
name: "unit",
environment: "node",
include: ["src/**/*.unit.test.ts"],
},
},
{
test: {
name: "browser",
environment: "jsdom",
include: ["src/**/*.browser.test.tsx"],
setupFiles: ["./src/test/setup.ts"],
},
},
]);
Key Takeaways
- Vitest is zero-config for Vite projects — just
npm install -D vitestand add test scripts vi.mock()is hoisted automatically, just likejest.mock()- Use
vi.spyOn()+vi.restoreAllMocks()inafterEachto avoid test pollution - Coverage thresholds in
vitest.config.tsenforce minimum quality gates in CI vitest --uiis worth running at least once — it changes how you navigate a large test suite- In-source testing with
import.meta.vitestis a great pattern for pure utility functions - For React testing:
jsdomenvironment +@testing-library/react+@testing-library/jest-domis the standard stack
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.