# SkillPatch skill: vitest-skill

This skill generates Vitest unit tests in JavaScript and TypeScript, leveraging Vite-native speed and a Jest-compatible API with ESM support. It provides concrete patterns for mocking (using vi.fn/vi.mock), snapshot testing, React component testing, in-source testing, and configuration. It also includes a quick-reference command table and anti-patterns guide to help agents avoid common mistakes.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/vitest-skill
curl -sSL https://skillpatch.dev/install_skill/vitest-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: vitest-skill
description: >
  Generates Vitest tests in JavaScript/TypeScript with Vite-native speed.
  Jest-compatible API with ESM support and HMR. Use when user mentions "Vitest",
  "vi.mock", "vitest.config". Triggers on: "Vitest", "vi.mock", "vi.fn",
  "Vite test", "vitest config".
languages:
  - JavaScript
  - TypeScript
category: unit-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Vitest Testing Skill

## Core Patterns

### Basic Test

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Calculator } from './calculator';

describe('Calculator', () => {
  let calc: Calculator;
  beforeEach(() => { calc = new Calculator(); });

  it('adds two numbers', () => {
    expect(calc.add(2, 3)).toBe(5);
  });

  it('throws on divide by zero', () => {
    expect(() => calc.divide(10, 0)).toThrow();
  });
});
```

### Mocking (vi instead of jest)

```typescript
import { vi } from 'vitest';

// Mock module
vi.mock('./database', () => ({
  getUser: vi.fn().mockResolvedValue({ name: 'Alice' }),
  saveUser: vi.fn().mockResolvedValue(true),
}));

// Mock function
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: 'test' });

// Spy
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
expect(spy).toHaveBeenCalledWith('message');
spy.mockRestore();

// Timers
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
vi.runAllTimers();
vi.useRealTimers();
```

### In-Source Testing

```typescript
// src/math.ts — tests alongside code!
export function add(a: number, b: number) { return a + b; }
export function multiply(a: number, b: number) { return a * b; }

if (import.meta.vitest) {
  const { it, expect } = import.meta.vitest;
  it('adds', () => { expect(add(2, 3)).toBe(5); });
  it('multiplies', () => { expect(multiply(3, 4)).toBe(12); });
}
```

### Snapshot Testing

```typescript
it('serializes user', () => {
  expect(serializeUser(user)).toMatchSnapshot();
});

it('inline snapshot', () => {
  expect(serializeUser(user)).toMatchInlineSnapshot(`
    { "name": "Alice", "email": "alice@test.com" }
  `);
});
```

### React Component Testing

```typescript
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import Button from './Button';

describe('Button', () => {
  it('renders with label', () => {
    render(<Button label="Click me" />);
    expect(screen.getByText('Click me')).toBeDefined();
  });
});
```

### Anti-Patterns

| Bad | Good | Why |
|-----|------|-----|
| `jest.fn()` | `vi.fn()` | Vitest uses `vi` |
| `jest.mock()` | `vi.mock()` | Different namespace |
| No type safety | TypeScript + strict | Vitest is TS-first |

## Quick Reference

| Task | Command |
|------|---------|
| Run once | `npx vitest run` |
| Watch | `npx vitest` (default) |
| UI | `npx vitest --ui` |
| Coverage | `npx vitest --coverage` |
| Specific file | `npx vitest run src/math.test.ts` |
| Filter | `npx vitest run -t "adds"` |

## vitest.config.ts

```typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: { provider: 'v8', reporter: ['text', 'html'] },
    include: ['src/**/*.{test,spec}.{js,ts,tsx}'],
    includeSource: ['src/**/*.{js,ts}'],
  },
});
```

## Deep Patterns → `reference/playbook.md`

| § | Section | Lines |
|---|---------|-------|
| 1 | Production Configuration | Config, workspace, setup file |
| 2 | Mocking Patterns | vi.mock, spies, timers, fetch |
| 3 | React Testing Library | Components, hooks, providers |
| 4 | Snapshot & Inline Snapshots | File, inline, serializers |
| 5 | Table-Driven Tests | test.each, describe.each |
| 6 | In-Source Testing | Co-located tests in source |
| 7 | API / Integration Testing | Server tests with fetch |
| 8 | CI/CD Integration | GitHub Actions, scripts |
| 9 | Debugging Quick-Reference | 10 common problems |
| 10 | Best Practices Checklist | 13 items |

````


### `reference/advanced-patterns.md`

````markdown
# Vitest — Advanced Patterns & Playbook

## In-Source Testing

```typescript
// src/utils.ts — tests live alongside code
export function sum(a: number, b: number) { return a + b; }

if (import.meta.vitest) {
  const { it, expect } = import.meta.vitest;
  it('adds numbers', () => expect(sum(1, 2)).toBe(3));
}
```

## Advanced Mocking

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

// Auto-mock entire module
vi.mock('./api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
  updateUser: vi.fn()
}));

// Partial mock — keep real implementations
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./utils')>();
  return { ...actual, formatDate: vi.fn(() => '2025-01-01') };
});

// Mock globals
vi.stubGlobal('fetch', vi.fn());
vi.stubGlobal('IntersectionObserver', vi.fn(() => ({
  observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn()
})));

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

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

// Spy on class methods
const spy = vi.spyOn(UserService.prototype, 'save');
spy.mockResolvedValue({ id: 1 });
```

## Concurrent & Parallel Testing

```typescript
// Run tests concurrently within a suite
describe.concurrent('independent API tests', () => {
  it('fetches users', async () => { /* ... */ });
  it('fetches products', async () => { /* ... */ });
  it('fetches orders', async () => { /* ... */ });
});

// Pool configuration in vitest.config.ts
export default defineConfig({
  test: {
    pool: 'threads',        // or 'forks', 'vmThreads'
    poolOptions: { threads: { maxThreads: 8, minThreads: 2 } },
    isolate: true,           // true = full isolation per test file
    fileParallelism: true
  }
});
```

## Snapshot Testing

```typescript
// Inline snapshot
it('creates user shape', () => {
  expect(createUser('Alice')).toMatchInlineSnapshot(`
    { "id": StringMatching /^[a-f0-9-]+$/, "name": "Alice" }
  `);
});

// File snapshot (saved to __snapshots__)
it('renders component', () => {
  const html = render(<Button variant="primary">Click</Button>);
  expect(html).toMatchSnapshot();
});

// Custom serializer
expect.addSnapshotSerializer({
  serialize: (val) => `MyType(${val.name})`,
  test: (val) => val?.__type === 'MyType'
});
```

## Vue/React Component Testing

```typescript
import { mount } from '@vue/test-utils';
// or: import { render, screen } from '@testing-library/react';

describe('Counter Component', () => {
  it('increments on click', async () => {
    const wrapper = mount(Counter, { props: { initial: 0 } });
    await wrapper.find('button').trigger('click');
    expect(wrapper.text()).toContain('1');
  });

  it('emits update event', async () => {
    const wrapper = mount(Counter);
    await wrapper.find('button').trigger('click');
    expect(wrapper.emitted('update')).toHaveLength(1);
    expect(wrapper.emitted('update')[0]).toEqual([1]);
  });
});
```

## Coverage & Configuration

```typescript
// vitest.config.ts — production-grade
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./test/setup.ts'],
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: ['node_modules/', 'test/', '**/*.d.ts', '**/*.config.*'],
      thresholds: { branches: 80, functions: 80, lines: 80, statements: 80 }
    },
    alias: { '@': path.resolve(__dirname, './src') },
    reporters: ['default', 'junit'],
    outputFile: { junit: './reports/junit.xml' },
    typecheck: { enabled: true }
  }
});
```

## Workspace Configuration (Monorepo)

```typescript
// vitest.workspace.ts
export default ['packages/*', 'apps/*'];

// Each package gets its own vitest.config.ts
// Run: vitest --workspace
```

## Anti-Patterns

- ❌ Using `jest.fn()` instead of `vi.fn()` — Vitest has its own API
- ❌ `import.meta.env` in tests without `vi.stubEnv` — env vars leak between tests
- ❌ `vi.mock()` inside `it()` — must be at file scope (hoisted automatically)
- ❌ Missing `vi.useRealTimers()` cleanup — fake timers leak to next test
- ❌ Not using `pool: 'forks'` for CPU-bound tests — threads share memory

````


### `reference/playbook.md`

````markdown
# Vitest — Advanced Implementation Playbook

## §1 — Production Configuration

```typescript
// 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)

```typescript
// 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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
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

```yaml
# 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/
```

```json
// 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

| Problem | Cause | Fix |
|---------|-------|-----|
| Mock not working | Module cached before mock | Move `vi.mock()` to top of file (hoisted) |
| `vi.mocked()` type error | Missing type assertion | Use `vi.mocked(fn)` with proper import |
| Timer test fails | Forgot `vi.useRealTimers()` | Add in `afterEach`, use `vi.useFakeTimers()` per test |
| Snapshot outdated | Code changed | Run `vitest -u` to update snapshots |
| jsdom missing APIs | `ResizeObserver`, `matchMedia` | Mock in setup file with `vi.stubGlobal()` |
| Act warning in React tests | State update outside act | Use `userEvent.setup()` and `waitFor()` |
| Module resolution fails | Missing alias | Add `resolve.alias` in vitest config |
| Coverage too low | Untested files | Set `coverage.all: true` to include all files |
| Tests slow | Large test suite | Use `pool: 'threads'`, parallel by default |
| In-source tests not found | Not enabled in config | Add `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`

````
