Skip to main content

Vitest Cheatsheet

Comprehensive quick reference for Vitest testing framework including matchers, mocking, hooks, snapshots, configuration, and best practices for Node.js and browser testing.

Table of Contents


Prerequisites

Vitest Version: This cheatsheet targets Vitest 4.0+. Some features require specific versions (noted inline).

Requirements:

  • Node.js: >= v20.0.0
  • Vite: >= v6.0.0
  • Package Manager: pnpm, npm, yarn, or bun
Terminal window
# Check Node.js version
node --version
# β†’ v20.0.0 or higher
# Check Vite version (if installed)
vite --version
# β†’ v6.0.0 or higher

Installation & Setup

Initial Setup πŸ”§

Terminal window
# Install Vitest as dev dependency
pnpm add -D vitest
# Install TypeScript types (optional)
pnpm add -D @types/node
# Add test script to package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}

Basic Configuration πŸ“

vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
exclude: ["**/node_modules/**", "**/dist/**"],
environment: "node", // 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime'
globals: true, // Enable global test APIs
},
});

With Existing Vite Config πŸ”„

vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from "vite";
export default defineConfig({
test: {
// Vitest-specific options
globals: true,
environment: "jsdom",
},
});

Run Tests πŸš€

Terminal window
# Run tests in watch mode (default)
pnpm test
# Run tests once
pnpm test:run
# Run tests with UI
pnpm test:ui
# Run specific test file
pnpm test src/utils.test.ts
# Run tests matching pattern
pnpm test --grep "user"
# Run tests in specific file
pnpm test src/utils.test.ts

Basic Test Structure

Test Cases πŸ“‹

import { describe, it, expect, test } from "vitest";
// Using 'test' (Jest-compatible)
test("adds 1 + 2 to equal 3", () => {
expect(1 + 2).toBe(3);
});
// Using 'it' (BDD style)
it("should return true", () => {
expect(true).toBe(true);
});
// Test with async/await
test("fetches user data", async () => {
const user = await fetchUser(1);
expect(user.id).toBe(1);
});

Test Suites πŸ—‚οΈ

import { describe, it, expect } from "vitest";
describe("Math utilities", () => {
it("adds two numbers", () => {
expect(add(2, 3)).toBe(5);
});
it("subtracts two numbers", () => {
expect(subtract(5, 2)).toBe(3);
});
// Nested describe blocks
describe("advanced operations", () => {
it("multiplies numbers", () => {
expect(multiply(2, 3)).toBe(6);
});
});
});

Skip & Only Tests ⏭️

import { describe, it, test } from "vitest";
// Skip a test
test.skip("skipped test", () => {
// This test won't run
});
// Run only this test
test.only("only this test runs", () => {
// Only this test executes
});
// Conditional skip
test.skipIf(process.env.CI)("skips in CI", () => {
// Skipped when CI=true
});
// Conditional run
test.runIf(process.env.DEBUG)("runs in debug", () => {
// Only runs when DEBUG=true
});

Todo Tests πŸ“

import { test } from "vitest";
// Mark test as todo (not implemented yet)
test.todo("implement user authentication");
test.todo("add error handling");

Concurrent Tests ⚑

import { describe, test } from "vitest";
// Run tests concurrently
describe("API tests", () => {
test.concurrent("test 1", async () => {
// Runs in parallel
});
test.concurrent("test 2", async () => {
// Runs in parallel
});
});
// Concurrent test suite
describe.concurrent("parallel suite", () => {
test("test 1", () => {});
test("test 2", () => {});
});

Matchers & Assertions

Equality Matchers βš–οΈ

import { expect, test } from "vitest";
// Strict equality (===)
test("strict equality", () => {
expect(1).toBe(1);
expect("hello").toBe("hello");
expect({}).not.toBe({}); // Different object references
});
// Deep equality
test("object equality", () => {
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
expect([1, 2, 3]).toEqual([1, 2, 3]);
});
// Object matching (partial)
test("object matching", () => {
expect({ a: 1, b: 2, c: 3 }).toMatchObject({ a: 1, b: 2 });
});

Truthiness Matchers βœ…

import { expect, test } from "vitest";
test("truthiness", () => {
expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect("hello").toBeTruthy();
expect({}).toBeTruthy();
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect("").toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
});
test("defined/undefined", () => {
expect("value").toBeDefined();
expect(undefined).toBeUndefined();
expect(null).not.toBeUndefined();
});

Nullability Matchers πŸ”

import { expect, test } from "vitest";
test("nullable values", () => {
expect(null).toBeNullable();
expect(undefined).toBeNullable();
expect("value").not.toBeNullable();
});

Number Matchers πŸ”’

import { expect, test } from "vitest";
test("number comparisons", () => {
expect(2 + 2).toBe(4);
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
expect(2 + 2).toBeLessThanOrEqual(4);
// Floating point equality
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});

String Matchers πŸ“

import { expect, test } from "vitest";
test("string matching", () => {
expect("hello world").toMatch("world");
expect("hello world").toMatch(/world/);
expect("hello").toContain("ell");
expect("hello").toHaveLength(5);
});

Array Matchers πŸ“Š

import { expect, test } from "vitest";
test("array assertions", () => {
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect([1, 2, 3]).toEqual([1, 2, 3]);
expect([1, 2, 3]).toContainEqual(2);
});

Object Matchers πŸ—‚οΈ

import { expect, test } from "vitest";
test("object properties", () => {
const user = { name: "John", age: 30, email: "john@example.com" };
expect(user).toHaveProperty("name");
expect(user).toHaveProperty("age", 30);
expect(user).toHaveProperty("name", "John");
});

Error Matchers ⚠️

import { expect, test } from "vitest";
test("error throwing", () => {
// Throw any error
expect(() => {
throw new Error("error message");
}).toThrow();
// Throw specific error message
expect(() => {
throw new Error("error message");
}).toThrow("error message");
// Throw error matching regex
expect(() => {
throw new Error("error message");
}).toThrow(/error/);
// Throw specific error type
expect(() => {
throw new TypeError("type error");
}).toThrow(TypeError);
});

Promise Matchers πŸ”„

import { expect, test } from "vitest";
async function fetchData() {
return Promise.resolve({ data: "test" });
}
async function fetchError() {
return Promise.reject(new Error("fetch failed"));
}
test("promise resolves", async () => {
await expect(fetchData()).resolves.toEqual({ data: "test" });
});
test("promise rejects", async () => {
await expect(fetchError()).rejects.toThrow("fetch failed");
});

One Of Matcher 🎯

import { expect, test } from "vitest";
test("value in array", () => {
expect("apple").toBeOneOf(["apple", "banana", "orange"]);
expect(5).toBeOneOf([1, 2, 3, 4, 5]);
});

Custom Matchers πŸ› οΈ

import { expect, test } from "vitest";
// Define custom matcher
expect.extend({
toBeFoo(received: string) {
const pass = received === "foo";
return {
message: () => `expected ${received} to be foo`,
pass,
};
},
});
// TypeScript types for custom matcher
interface CustomMatchers<R = unknown> {
toBeFoo(): R;
}
declare module "vitest" {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
test("custom matcher", () => {
expect("foo").toBeFoo();
});

Mocking

Mock Functions 🎭

import { vi, describe, it, expect } from "vitest";
// Create mock function
const mockFn = vi.fn();
// Basic usage
mockFn("arg1", "arg2");
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
// Mock implementation
const mockFn2 = vi.fn((x) => x * 2);
expect(mockFn2(5)).toBe(10);
// Mock return value
const mockFn3 = vi.fn();
mockFn3.mockReturnValue(42);
expect(mockFn3()).toBe(42);
// Mock resolved value (async)
const mockAsync = vi.fn();
mockAsync.mockResolvedValue({ data: "success" });
await expect(mockAsync()).resolves.toEqual({ data: "success" });
// Mock rejected value
const mockError = vi.fn();
mockError.mockRejectedValue(new Error("failed"));
await expect(mockError()).rejects.toThrow("failed");

Spying on Functions πŸ”

import { vi, describe, it, expect } from "vitest";
const obj = {
greet: (name: string) => `Hello ${name}`,
calculate: (a: number, b: number) => a + b,
};
// Spy on method
const greetSpy = vi.spyOn(obj, "greet");
obj.greet("Alice");
expect(greetSpy).toHaveBeenCalledWith("Alice");
expect(greetSpy).toHaveBeenCalledTimes(1);
// Mock implementation
const calcSpy = vi.spyOn(obj, "calculate").mockImplementation((a, b) => a * b);
expect(obj.calculate(2, 3)).toBe(6); // Returns 6 instead of 5
// Restore original
calcSpy.mockRestore();
expect(obj.calculate(2, 3)).toBe(5); // Back to original behavior

Mocking Modules πŸ“¦

import { vi, describe, it, expect } from "vitest";
// Mock entire module
vi.mock("./utils", () => ({
fetchData: vi.fn(() => Promise.resolve({ data: "mocked" })),
processData: vi.fn((data) => data.toUpperCase()),
}));
// Mock with original implementation
vi.mock("./api", async () => {
const actual = await vi.importActual("./api");
return {
...actual,
fetchUser: vi.fn(),
};
});
// Mock module with factory
vi.mock("./logger", () => ({
default: {
log: vi.fn(),
error: vi.fn(),
},
}));

Mocking Classes πŸ›οΈ

import { vi, describe, it, expect } from "vitest";
// Mock class constructor
class UserService {
getUser(id: number) {
return { id, name: "User" };
}
}
vi.mock("./UserService", () => ({
UserService: vi.fn().mockImplementation(() => ({
getUser: vi.fn(() => ({ id: 1, name: "Mocked User" })),
})),
}));
// Spy on class method
const userService = new UserService();
const getUserSpy = vi.spyOn(userService, "getUser").mockReturnValue({
id: 1,
name: "Spied User",
});

Mocking Properties πŸ”‘

import { vi, describe, it, expect } from "vitest";
const obj = {
get value() {
return "original";
},
};
// Mock getter
const valueSpy = vi.spyOn(obj, "value", "get").mockReturnValue("mocked");
expect(obj.value).toBe("mocked");
// Mock setter
const setterSpy = vi.spyOn(obj, "value", "set").mockImplementation(() => {});
obj.value = "new value";
expect(setterSpy).toHaveBeenCalled();

Mock Timers ⏰

import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("advances time", () => {
const callback = vi.fn();
setTimeout(callback, 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
it("runs all timers", () => {
const callback = vi.fn();
setTimeout(callback, 1000);
vi.runAllTimers();
expect(callback).toHaveBeenCalled();
});
it("advances to next timer", () => {
const callback = vi.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
vi.advanceTimersToNextTimer();
expect(callback).toHaveBeenCalledTimes(1);
});

Mock Date πŸ“…

import { vi, describe, it, expect } from "vitest";
it("mocks date", () => {
const mockDate = new Date("2024-01-01");
vi.setSystemTime(mockDate);
expect(new Date()).toEqual(mockDate);
vi.useRealTimers(); // Restore real timers
});

Mock Globals 🌐

import { vi, describe, it, expect } from "vitest";
// Mock fetch
global.fetch = vi.fn();
it("mocks fetch", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: "test" }),
} as Response);
const response = await fetch("https://api.example.com");
const data = await response.json();
expect(data).toEqual({ data: "test" });
});
// Mock window object
Object.defineProperty(window, "location", {
value: {
href: "https://example.com",
pathname: "/test",
},
writable: true,
});

Mock Reset & Restore πŸ”„

import { vi, describe, it, expect, beforeEach } from "vitest";
const mockFn = vi.fn();
beforeEach(() => {
// Reset call history but keep implementation
mockFn.mockReset();
// Restore original implementation (for spies)
// mockFn.mockRestore()
// Clear all mocks (reset + restore)
// vi.clearAllMocks()
});

Test Hooks & Lifecycle

beforeAll & afterAll 🎯

import { describe, it, expect, beforeAll, afterAll } from "vitest";
describe("Database tests", () => {
let db: Database;
beforeAll(async () => {
// Runs once before all tests in suite
db = await connectDatabase();
await db.migrate();
});
afterAll(async () => {
// Runs once after all tests in suite
await db.close();
});
it("inserts data", async () => {
await db.insert({ name: "Test" });
});
it("queries data", async () => {
const data = await db.query("SELECT * FROM users");
expect(data).toBeDefined();
});
});

beforeEach & afterEach πŸ”„

import { describe, it, expect, beforeEach, afterEach } from "vitest";
describe("User service", () => {
let userService: UserService;
beforeEach(async () => {
// Runs before each test
userService = new UserService();
await userService.clear();
});
afterEach(async () => {
// Runs after each test
await userService.cleanup();
});
it("creates user", async () => {
const user = await userService.create({ name: "John" });
expect(user.id).toBeDefined();
});
it("updates user", async () => {
const user = await userService.create({ name: "Jane" });
await userService.update(user.id, { name: "Jane Doe" });
const updated = await userService.findById(user.id);
expect(updated.name).toBe("Jane Doe");
});
});

Cleanup with Return Function 🧹

import { beforeEach } from "vitest";
beforeEach(async () => {
const db = await setupDatabase();
// Return cleanup function (runs after each test)
return async () => {
await db.close();
};
});

onTestFinished Hook 🏁

import { test, onTestFinished } from "vitest";
test("performs query", () => {
const db = connectDb();
// Register cleanup callback
onTestFinished(() => {
db.close();
});
db.query("SELECT * FROM users");
});
// Reusable cleanup helper
function getTestDb() {
const db = connectMockedDb();
onTestFinished(() => db.close());
return db;
}

Snapshots

Basic Snapshots πŸ“Έ

import { expect, test } from "vitest";
test("matches snapshot", () => {
const data = { foo: "bar", nested: { value: 123 } };
expect(data).toMatchSnapshot();
});
// Generated snapshot file: __snapshots__/test.snap.ts
// exports['matches snapshot 1'] = `
// Object {
// "foo": "bar",
// "nested": Object {
// "value": 123,
// },
// }
// `

Inline Snapshots πŸ“

import { expect, test } from "vitest";
test("inline snapshot", () => {
const result = formatData({ name: "John", age: 30 });
expect(result).toMatchInlineSnapshot(`
"Name: John, Age: 30"
`);
});

File Snapshots πŸ“„

import { expect, test } from "vitest";
test("file snapshot", async () => {
const html = renderComponent();
await expect(html).toMatchFileSnapshot("./snapshots/component.html");
});

Snapshot with Hints 🏷️

import { expect, test } from "vitest";
test("snapshot with hint", () => {
const data = { version: "1.0.0" };
expect(data).toMatchSnapshot("version data");
});
// Snapshot name: "snapshot with hint: version data"

Partial Snapshots 🎯

import { expect, test } from "vitest";
test("partial snapshot", () => {
const data = {
id: 1,
name: "John",
metadata: { created: "2024-01-01" },
};
// Match only specific properties
expect(data).toMatchSnapshot({
metadata: expect.any(Object),
});
});

Error Snapshots ⚠️

import { expect, test } from "vitest";
test("error snapshot", () => {
expect(() => {
throw new Error("Something went wrong");
}).toThrowErrorMatchingSnapshot();
});

Update Snapshots πŸ”„

Terminal window
# Update all snapshots
pnpm test --update
# Update snapshots for specific file
pnpm test src/utils.test.ts --update

Configuration

Basic Config πŸ“‹

vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Test file patterns
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"],
// Test environment
environment: "node", // 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime'
// Global test APIs
globals: true,
// Setup files
setupFiles: ["./test/setup.ts"],
globalSetup: ["./test/global-setup.ts"],
// Timeouts
testTimeout: 5000,
hookTimeout: 10000,
// Execution
pool: "forks", // 'forks' | 'threads' | 'vmThreads'
poolOptions: {
forks: {
singleFork: true,
},
},
},
});

Environment Configuration 🌍

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Different environments per test file
environmentMatchGlobs: [
["**/*.dom.test.ts", "jsdom"],
["**/*.node.test.ts", "node"],
],
// Environment options
environmentOptions: {
jsdom: {
url: "http://localhost:3000",
},
},
},
});

Reporters πŸ“Š

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
reporters: [
"default",
// Conditional reporter
process.env.CI ? "github-actions" : {},
// Custom reporter with options
["json", { outputFile: "./test-results.json" }],
["html", { outputDir: "./test-results" }],
],
},
});

Test Sequencing πŸ”€

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
sequence: {
// Shuffle tests
shuffle: false,
// Run tests concurrently
concurrent: false,
// Random seed for reproducibility
seed: 12345,
// Hook execution order
hooks: "stack", // 'stack' | 'parallel' | 'sequence'
},
},
});

Workspace Configuration πŸ—‚οΈ

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Multi-project workspace
workspace: [
"packages/*",
{
test: {
name: "project-a",
include: ["packages/a/**/*.test.ts"],
},
},
],
},
});

TypeScript Configuration πŸ”·

vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
typecheck: {
enabled: true,
tsconfig: "./tsconfig.test.json",
},
},
});

Coverage

Setup Coverage πŸ“ˆ

Terminal window
# Install coverage provider
pnpm add -D @vitest/coverage-v8
# or
pnpm add -D @vitest/coverage-istanbul

Coverage Configuration βš™οΈ

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
// Coverage provider
provider: "v8", // 'v8' | 'istanbul' | 'custom'
// Enable coverage
enabled: true,
// Coverage reporters
reporter: ["text", "json", "html", "lcov"],
// Output directory
reportsDirectory: "./coverage",
// Files to include/exclude
include: ["src/**/*.{ts,tsx}"],
exclude: [
"**/*.test.ts",
"**/*.spec.ts",
"**/node_modules/**",
"**/dist/**",
],
// Coverage thresholds
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
// Per-file thresholds
perFile: true,
// 100% coverage required
all: true,
},
},
});

Run Coverage πŸ“Š

Terminal window
# Run tests with coverage
pnpm test --coverage
# Run coverage once
pnpm test:run --coverage
# Update coverage thresholds
pnpm test --coverage --coverage.thresholds.lines=90

Custom Coverage Provider πŸ› οΈ

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "custom",
customProviderModule: "./custom-coverage-provider.ts",
},
},
});

Test Utilities

Test Context 🎯

import { test } from "vitest";
test("with context", ({ expect, task, onTestFinished }) => {
// Access test utilities from context
expect(1 + 1).toBe(2);
// Test metadata
console.log(task.name); // Test name
console.log(task.file); // Test file path
// Cleanup hook
onTestFinished(() => {
console.log("Test finished");
});
});

Extended Test Context πŸ”§

import { test as baseTest } from "vitest";
// Define custom fixtures
const test = baseTest.extend<{
db: Database;
user: User;
}>({
db: async ({}, use) => {
const db = await setupDatabase();
await use(db);
await db.close();
},
user: async ({ db }, use) => {
const user = await db.createUser({ name: "Test User" });
await use(user);
await db.deleteUser(user.id);
},
});
// Use extended test
test("user operations", async ({ db, user }) => {
const found = await db.findUser(user.id);
expect(found).toEqual(user);
});

Test Retry πŸ”

import { test } from "vitest";
// Retry flaky tests
test(
"flaky test",
async () => {
// Test that might fail occasionally
},
{ retry: 3 },
);

Test Timeout ⏱️

import { test } from "vitest";
// Custom timeout per test
test(
"slow test",
async () => {
// Long-running test
},
{ timeout: 10000 },
);

Test Repeat πŸ”„

import { test } from "vitest";
// Repeat test multiple times
test(
"repeat test",
() => {
// Test logic
},
{ repeats: 5 },
);

Best Practices

βœ… Do’s

βœ… Use descriptive test names

// Good
test("returns user by id when user exists", () => {});
// Bad
test("test user", () => {});

βœ… Keep tests isolated

// Each test should be independent
test("test 1", () => {
const result = calculate(2, 3);
expect(result).toBe(5);
});
test("test 2", () => {
// Doesn't depend on test 1
const result = calculate(4, 5);
expect(result).toBe(9);
});

βœ… Use beforeEach for common setup

describe("UserService", () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
test("creates user", () => {
const user = userService.create({ name: "John" });
expect(user).toBeDefined();
});
});

βœ… Mock external dependencies

vi.mock("./api", () => ({
fetchUser: vi.fn(),
}));
test("uses mocked API", async () => {
vi.mocked(fetchUser).mockResolvedValue({ id: 1 });
// Test logic
});

βœ… Use appropriate matchers

// Use toEqual for objects/arrays
expect({ a: 1 }).toEqual({ a: 1 });
// Use toBe for primitives
expect(1).toBe(1);

βœ… Clean up after tests

afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});

❌ Don’ts

❌ Don’t test implementation details

// Bad - testing internal state
test("increments counter", () => {
const counter = new Counter();
counter.increment();
expect(counter._count).toBe(1); // Private property
});
// Good - testing behavior
test("increments counter", () => {
const counter = new Counter();
counter.increment();
expect(counter.getValue()).toBe(1);
});

❌ Don’t share mutable state

// Bad
let sharedData: any;
test("test 1", () => {
sharedData = { value: 1 };
});
test("test 2", () => {
expect(sharedData.value).toBe(1); // Depends on test 1
});
// Good
test("test 1", () => {
const data = { value: 1 };
expect(data.value).toBe(1);
});

❌ Don’t use magic numbers

// Bad
expect(result).toBe(42);
// Good
const EXPECTED_RESULT = 42;
expect(result).toBe(EXPECTED_RESULT);

❌ Don’t ignore errors

// Bad
test("handles error", () => {
try {
riskyOperation();
} catch (e) {
// Ignored
}
});
// Good
test("handles error", () => {
expect(() => riskyOperation()).toThrow();
});

❌ Don’t test multiple things in one test

// Bad
test("user operations", () => {
const user = createUser();
updateUser(user.id);
deleteUser(user.id);
expect(getUser(user.id)).toBeUndefined();
});
// Good - split into multiple tests
test("creates user", () => {});
test("updates user", () => {});
test("deletes user", () => {});

Common Pitfalls

⚠️ Mocking Internal Calls

Warning: Methods called internally within the same file cannot be mocked from outside.

utils.ts
export function foo() {
return "foo";
}
export function foobar() {
return `${foo()}bar`; // Internal call
}
// test.ts
// ❌ This won't affect the internal foo() call
vi.spyOn(mod, "foo").mockReturnValue("mocked");
// βœ… Mock the entire module instead
vi.mock("./utils", () => ({
foo: vi.fn(() => "mocked"),
foobar: vi.fn(() => "mockedbar"),
}));

⚠️ Async Test Cleanup

// ❌ Missing await
test("async test", async () => {
const result = await fetchData();
expect(result).toBeDefined();
// Cleanup might not complete
cleanup();
});
// βœ… Proper async cleanup
test("async test", async () => {
const result = await fetchData();
expect(result).toBeDefined();
await cleanup();
});

⚠️ Timer Mocking

// ❌ Forgetting to restore timers
test("timer test", () => {
vi.useFakeTimers();
// Test logic
// Timers not restored!
});
// βœ… Always restore timers
test("timer test", () => {
vi.useFakeTimers();
try {
// Test logic
} finally {
vi.useRealTimers();
}
});
// βœ… Or use beforeEach/afterEach
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});

⚠️ Concurrent Test State

// ❌ Shared mutable state in concurrent tests
let counter = 0;
test.concurrent("test 1", () => {
counter++;
expect(counter).toBe(1); // Might fail!
});
test.concurrent("test 2", () => {
counter++;
expect(counter).toBe(1); // Might fail!
});
// βœ… Use test context or fixtures
test.concurrent("test 1", ({ task }) => {
const counter = 0;
counter++;
expect(counter).toBe(1);
});

⚠️ Snapshot Updates

Warning: Always review snapshot changes before committing. Snapshots can hide bugs if updated blindly.

Terminal window
# ❌ Blindly updating snapshots
pnpm test --update
# βœ… Review changes first
pnpm test
# Review failed snapshots
# Then update if correct
pnpm test --update

⚠️ Global Mocks

// ❌ Global mock affecting all tests
vi.mock("./api"); // Affects all test files
// βœ… Use vi.doMock for dynamic mocking
test("specific test", async () => {
vi.doMock("./api", () => ({
fetchData: vi.fn(),
}));
// Test logic
vi.doUnmock("./api");
});