← All skills

Vitest Skill

Unit testingJavaScriptTypeScript

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill vitest-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Vitest — Advanced Implementation Playbook

§1 — Production Configuration

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: { '@': resolve(__dirname, 'src') },
  },
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
    exclude: ['tests/e2e/**', 'node_modules/**'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov', 'json-summary'],
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['src/**/*.d.ts', 'src/**/index.ts', 'src/**/*.stories.tsx'],
      thresholds: { lines: 80, branches: 75, functions: 80, statements: 80 },
    },
    pool: 'threads',
    poolOptions: { threads: { maxThreads: 4, minThreads: 1 } },
    reporters: ['default', 'json', 'junit'],
    outputFile: {
      json: './test-results/results.json',
      junit: './test-results/junit.xml',
    },
    typecheck: { enabled: true },
  },
});

Workspace Config (Monorepo)

// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  { extends: './vitest.config.ts', test: { name: 'unit', include: ['tests/unit/**/*.test.ts'] } },
  { extends: './vitest.config.ts', test: { name: 'integration', include: ['tests/integration/**/*.test.ts'], environment: 'node' } },
  { extends: './vitest.config.ts', test: { name: 'components', include: ['tests/components/**/*.test.tsx'], environment: 'jsdom' } },
]);

Setup File

// tests/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';

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

// Global mocks
vi.stubGlobal('ResizeObserver', vi.fn().mockImplementation(() => ({
  observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(),
})));

vi.stubGlobal('IntersectionObserver', vi.fn().mockImplementation(() => ({
  observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(),
})));

Object.defineProperty(window, 'matchMedia', {
  value: vi.fn().mockImplementation(query => ({
    matches: false, media: query,
    addEventListener: vi.fn(), removeEventListener: vi.fn(),
  })),
});

§2 — Mocking Patterns

import { describe, it, expect, vi, beforeEach } from 'vitest';

// Module mock
vi.mock('@/services/api', () => ({
  fetchUser: vi.fn(),
  fetchProducts: vi.fn(),
}));

import { fetchUser, fetchProducts } from '@/services/api';
const mockFetchUser = vi.mocked(fetchUser);

describe('UserService', () => {
  beforeEach(() => { vi.clearAllMocks(); });

  it('should fetch and transform user', async () => {
    mockFetchUser.mockResolvedValue({ id: 1, name: 'Alice', email: 'a@test.com' });
    const result = await userService.getUser(1);
    expect(result).toEqual({ id: 1, displayName: 'Alice', email: 'a@test.com' });
    expect(mockFetchUser).toHaveBeenCalledWith(1);
  });

  it('should handle errors', async () => {
    mockFetchUser.mockRejectedValue(new Error('Network error'));
    await expect(userService.getUser(1)).rejects.toThrow('Network error');
  });
});

// Partial module mock (keep original exports)
vi.mock('@/utils/helpers', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/utils/helpers')>();
  return { ...actual, formatDate: vi.fn().mockReturnValue('2024-01-01') };
});

// Spy on object methods
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

// Fetch mock
const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);

mockFetch.mockResolvedValueOnce({
  ok: true,
  json: () => Promise.resolve({ data: [1, 2, 3] }),
  status: 200,
});

// Timer mocking
describe('Debounce', () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { vi.useRealTimers(); });

  it('should debounce calls', () => {
    const fn = vi.fn();
    const debounced = debounce(fn, 300);
    debounced(); debounced(); debounced();
    expect(fn).not.toHaveBeenCalled();
    vi.advanceTimersByTime(300);
    expect(fn).toHaveBeenCalledOnce();
  });
});

// Date mocking
vi.setSystemTime(new Date('2024-06-15T12:00:00Z'));
expect(new Date().toISOString()).toContain('2024-06-15');
vi.useRealTimers();

§3 — React Testing Library Integration

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';

describe('LoginForm', () => {
  it('should submit form with valid credentials', async () => {
    const onSubmit = vi.fn();
    const user = userEvent.setup();

    render(<LoginForm onSubmit={onSubmit} />);

    await user.type(screen.getByLabelText(/email/i), 'alice@test.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    await user.click(screen.getByRole('button', { name: /login/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'alice@test.com',
      password: 'password123',
    });
  });

  it('should show validation errors', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.click(screen.getByRole('button', { name: /login/i }));

    expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });
});

// Custom hook testing
import { renderHook, act } from '@testing-library/react';

describe('useCounter', () => {
  it('should increment', () => {
    const { result } = renderHook(() => useCounter(0));
    act(() => { result.current.increment(); });
    expect(result.current.count).toBe(1);
  });
});

// Testing with context providers
function renderWithProviders(ui: React.ReactElement, options = {}) {
  return render(ui, {
    wrapper: ({ children }) => (
      <AuthProvider><ThemeProvider>{children}</ThemeProvider></AuthProvider>
    ),
    ...options,
  });
}

§4 — Snapshot & Inline Snapshots

// File snapshot
it('should match component snapshot', () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(container.firstChild).toMatchSnapshot();
});

// Inline snapshot (auto-updated by vitest)
it('should format user display', () => {
  expect(formatUser({ name: 'Alice', role: 'admin' }))
    .toMatchInlineSnapshot(`"Alice (admin)"`);
});

// Snapshot with custom serializer
expect.addSnapshotSerializer({
  serialize(val) { return `User: ${val.name}`; },
  test(val) { return val && val.hasOwnProperty('name'); },
});

§5 — Table-Driven & Parameterized Tests

// test.each with array
it.each([
  [1, 1, 2],
  [2, 3, 5],
  [0, 0, 0],
  [-1, 1, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

// test.each with objects
it.each([
  { input: 'hello', expected: 'HELLO' },
  { input: 'world', expected: 'WORLD' },
  { input: '', expected: '' },
])('toUpper("$input") → "$expected"', ({ input, expected }) => {
  expect(input.toUpperCase()).toBe(expected);
});

// describe.each
describe.each([
  { role: 'admin', canDelete: true, canEdit: true },
  { role: 'editor', canDelete: false, canEdit: true },
  { role: 'viewer', canDelete: false, canEdit: false },
])('Role: $role', ({ role, canDelete, canEdit }) => {
  it(`canDelete: ${canDelete}`, () => {
    expect(permissions(role).canDelete).toBe(canDelete);
  });
  it(`canEdit: ${canEdit}`, () => {
    expect(permissions(role).canEdit).toBe(canEdit);
  });
});

§6 — In-Source Testing

// src/utils/math.ts
export function add(a: number, b: number): number { return a + b; }
export function multiply(a: number, b: number): number { return a * b; }

// Tests co-located in source file
if (import.meta.vitest) {
  const { it, expect, describe } = import.meta.vitest;

  describe('math utils', () => {
    it('add', () => { expect(add(1, 2)).toBe(3); });
    it('multiply', () => { expect(multiply(3, 4)).toBe(12); });
  });
}

// Enable in config:
// defineConfig({ test: { includeSource: ['src/**/*.ts'] } })
// For production build, tree-shake with:
// define: { 'import.meta.vitest': 'undefined' }

§7 — API / Integration Testing

import { describe, it, expect, beforeAll, afterAll } from 'vitest';

describe('API Integration', () => {
  let server: any;
  let baseUrl: string;

  beforeAll(async () => {
    server = await startTestServer();
    baseUrl = `http://localhost:${server.port}`;
  });

  afterAll(async () => { await server.close(); });

  it('should create and fetch user', async () => {
    // Create
    const createRes = await fetch(`${baseUrl}/api/users`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Alice', email: 'alice@test.com' }),
    });
    expect(createRes.status).toBe(201);
    const { id } = await createRes.json();

    // Fetch
    const getRes = await fetch(`${baseUrl}/api/users/${id}`);
    expect(getRes.status).toBe(200);
    const user = await getRes.json();
    expect(user.name).toBe('Alice');
  });
});

§8 — CI/CD Integration

# GitHub Actions
name: Vitest
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx vitest run --coverage --reporter=junit
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: |
            test-results/
            coverage/
// package.json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage",
    "test:ci": "vitest run --coverage --reporter=junit --reporter=default"
  }
}

§9 — Debugging Quick-Reference

ProblemCauseFix
Mock not workingModule cached before mockMove vi.mock() to top of file (hoisted)
vi.mocked() type errorMissing type assertionUse vi.mocked(fn) with proper import
Timer test failsForgot vi.useRealTimers()Add in afterEach, use vi.useFakeTimers() per test
Snapshot outdatedCode changedRun vitest -u to update snapshots
jsdom missing APIsResizeObserver, matchMediaMock in setup file with vi.stubGlobal()
Act warning in React testsState update outside actUse userEvent.setup() and waitFor()
Module resolution failsMissing aliasAdd resolve.alias in vitest config
Coverage too lowUntested filesSet coverage.all: true to include all files
Tests slowLarge test suiteUse pool: 'threads', parallel by default
In-source tests not foundNot enabled in configAdd includeSource to test config

§10 — Best Practices Checklist

  • ✅ Use vi.fn() / vi.mock() — Jest-compatible API
  • ✅ Use vi.clearAllMocks() in afterEach for clean state
  • ✅ Use vi.mocked() for type-safe mock access
  • ✅ Use pool: 'threads' for parallel execution (default)
  • ✅ Use @testing-library/react with userEvent.setup() for React tests
  • ✅ Use inline snapshots for small, readable assertions
  • ✅ Use test.each / describe.each for parameterized tests
  • ✅ Use workspace config for monorepo projects
  • ✅ Use in-source testing for utility functions
  • ✅ Use --ui flag for interactive test explorer
  • ✅ Use vi.stubGlobal() for browser API mocks in setup
  • ✅ Configure coverage thresholds in vitest.config.ts
  • ✅ Structure: tests/unit/, tests/components/, tests/integration/, tests/setup.ts