Vitest Setup & Basics
Vitest is a Vite-native test runner with Jest-compatible APIs, native TypeScript support, and fast HMR-based watch mode. TS + JS
// Install
// npm i -D vitest
// vitest.config.ts — project configuration
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // use describe/it/expect without imports
environment: "node", // or "jsdom", "happy-dom"
include: ["src/**/*.test.{ts,js}"],
coverage: {
provider: "v8", // or "istanbul"
reporter: ["text", "html", "lcov"],
thresholds: { lines: 80, branches: 75, functions: 80 },
},
},
});
// src/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/math.test.ts — test file
import { describe, it, expect } from "vitest";
import { add, divide } from "./math";
describe("math utilities", () => {
it("adds two numbers", () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
it("divides two numbers", () => {
expect(divide(10, 2)).toBe(5);
expect(divide(7, 2)).toBeCloseTo(3.5);
});
it("throws on division by zero", () => {
expect(() => divide(1, 0)).toThrow("Division by zero");
});
it.each([
[1, 2, 3],
[0, 0, 0],
[-5, 5, 0],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
Testing Async Code
Strategies for testing promises, timers, and network requests without hitting real services. TS + JS
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Testing promises — just await the result
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`User ${id} not found`);
return res.json();
}
describe("fetchUser", () => {
beforeEach(() => {
// Mock global fetch
vi.stubGlobal("fetch", vi.fn());
});
afterEach(() => vi.restoreAllMocks());
it("returns user data", async () => {
const mockUser = { id: "1", name: "Ada" };
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify(mockUser), { status: 200 })
);
const user = await fetchUser("1");
expect(user).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith("/api/users/1");
});
it("throws on 404", async () => {
vi.mocked(fetch).mockResolvedValue(
new Response(null, { status: 404 })
);
await expect(fetchUser("999")).rejects.toThrow("User 999 not found");
});
});
// Fake timers — control setTimeout, setInterval, Date
describe("debounce", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("delays execution", () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
it("works with Date.now()", () => {
vi.setSystemTime(new Date("2025-01-15"));
expect(new Date().getFullYear()).toBe(2025);
});
});
Mocking Patterns
Replace dependencies with controlled substitutes for isolated, deterministic testing. TS + JS
import { describe, it, expect, vi } from "vitest";
// vi.fn() — create a standalone mock function
const callback = vi.fn();
callback("hello");
expect(callback).toHaveBeenCalledWith("hello");
expect(callback).toHaveBeenCalledTimes(1);
// Mock with implementation
const rand = vi.fn(() => 0.42);
expect(rand()).toBe(0.42);
// Mock return sequence
const getId = vi.fn()
.mockReturnValueOnce("id-1")
.mockReturnValueOnce("id-2")
.mockReturnValue("id-default");
// vi.mock() — mock entire modules
vi.mock("./database", () => ({
db: {
query: vi.fn().mockResolvedValue([{ id: 1 }]),
close: vi.fn(),
},
}));
// vi.spyOn() — observe real methods, optionally override
import * as utils from "./utils";
const spy = vi.spyOn(utils, "formatDate");
spy.mockReturnValue("2025-01-01");
expect(utils.formatDate(new Date())).toBe("2025-01-01");
expect(spy).toHaveBeenCalledOnce();
spy.mockRestore(); // restore original implementation
// Mock class instance
vi.mock("./EmailService", () => ({
EmailService: vi.fn().mockImplementation(() => ({
send: vi.fn().mockResolvedValue({ delivered: true }),
verify: vi.fn().mockResolvedValue(true),
})),
}));
Caution: Over-mocking leads to tests that pass but don't catch real bugs. Mock at boundaries (network, filesystem, clock) and prefer real implementations for pure logic.
Type Testing & Code Coverage
Verify that your types behave correctly at compile time, and enforce minimum test coverage thresholds. TS Only
// Install: npm i -D expect-type
import { expectTypeOf } from "expect-type";
import { add, fetchUser } from "./math";
// Test return types
expectTypeOf(add).returns.toBeNumber();
expectTypeOf(fetchUser).returns.resolves.toMatchTypeOf<{ id: string }>();
// Test parameter types
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>();
// Test that a type IS an error (should not compile)
type OnlyStrings<T> = T extends string ? T : never;
expectTypeOf<OnlyStrings<number>>().toBeNever();
// Test assignability
expectTypeOf<{ a: 1 }>().toMatchTypeOf<{ a: number }>();
expectTypeOf<string>().not.toEqualTypeOf<number>();
// Test generic constraints
interface Repository<T extends { id: string }> {
find(id: string): T | null;
}
expectTypeOf<Repository<{ id: string; name: string }>>().toBeObject();
// === Code Coverage ===
// vitest.config.ts thresholds
// coverage: {
// provider: "v8",
// thresholds: {
// lines: 80,
// branches: 75,
// functions: 80,
// statements: 80,
// },
// }
// Run coverage:
// npx vitest run --coverage
// npx vitest run --coverage.provider=istanbul # alt provider
// Per-file thresholds (vitest 1.4+)
// coverage: {
// thresholds: {
// "src/critical/**": { lines: 95, branches: 90 },
// "src/utils/**": { lines: 70 },
// }
// }
Note: Type tests run at compile time, not runtime. They ensure your type definitions and generics behave as intended without executing any code. Combine with runtime tests for full confidence.
Testing DOM & Components
Test browser-like environments using jsdom or happy-dom, simulating user events and verifying rendered output. TS + JS
// vitest.config.ts — use jsdom or happy-dom
// test: { environment: "happy-dom" } // faster, less complete
// test: { environment: "jsdom" } // more compatible
import { describe, it, expect, beforeEach } from "vitest";
// Testing DOM manipulation
function createCounter(container: HTMLElement) {
let count = 0;
container.innerHTML = `
<span class="count">0</span>
<button class="inc">+</button>
<button class="dec">-</button>
`;
container.querySelector(".inc")!.addEventListener("click", () => {
count++;
container.querySelector(".count")!.textContent = String(count);
});
container.querySelector(".dec")!.addEventListener("click", () => {
count--;
container.querySelector(".count")!.textContent = String(count);
});
}
describe("createCounter", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
createCounter(container);
});
it("starts at 0", () => {
expect(container.querySelector(".count")!.textContent).toBe("0");
});
it("increments on click", () => {
container.querySelector<HTMLButtonElement>(".inc")!.click();
container.querySelector<HTMLButtonElement>(".inc")!.click();
expect(container.querySelector(".count")!.textContent).toBe("2");
});
it("decrements on click", () => {
container.querySelector<HTMLButtonElement>(".dec")!.click();
expect(container.querySelector(".count")!.textContent).toBe("-1");
});
it("responds to keyboard events", () => {
const event = new KeyboardEvent("keydown", { key: "ArrowUp" });
document.dispatchEvent(event);
// test your keyboard handler logic
});
});
Performance: happy-dom is 2-10x faster than jsdom for most tests. Use jsdom only when you need full browser compatibility (e.g., canvas, IntersectionObserver polyfills). Set environment per-file with // @vitest-environment happy-dom at the top.