APM

>Agent Skill

@thamjiahe/vitest-unit-testing

skilldevelopment

Write fast unit tests with Vitest, coverage reporting, mocking, and snapshots. Apply when testing utilities, components, services, or building test coverage.

Claude Opus 4.5, Claude Code v2.x
apm::install
$apm install @thamjiahe/vitest-unit-testing
apm::allowed-tools
ReadWriteEditBash
apm::skill.md
---
name: "Vitest Unit Testing"
description: "Write fast unit tests with Vitest, coverage reporting, mocking, and snapshots. Apply when testing utilities, components, services, or building test coverage."
allowed-tools: Read, Write, Edit, Bash
version: 1.1.0
compatibility: Claude Opus 4.5, Claude Code v2.x
updated: 2026-01-24
---

# Vitest Unit Testing

Systematic unit testing with Vitest for fast, reliable test feedback.

## Overview

This Skill enforces:
- Test-driven development (TDD) with Vitest
- Component testing with React
- Mocking and spying
- Snapshot testing
- Code coverage reporting
- Watch mode for development
- Parallel test execution

Apply when writing unit tests, testing components, or building test coverage.

## Setup Vitest

### Install Dependencies

```bash
npm install -D vitest @vitest/ui
npm install -D @testing-library/react @testing-library/jest-dom
npm install -D jsdom  # For DOM testing
```

### Configure vitest.config.ts

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

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./vitest.setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.test.ts',
        '**/*.spec.ts'
      ]
    }
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
});
```

### Setup File

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

// Cleanup after each test
afterEach(() => {
  cleanup();
});
```

### Package.json Scripts

```json
{
  "scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}
```

## Writing Unit Tests

### Basic Test Structure

```ts
// src/utils/helpers.test.ts
import { describe, it, expect } from 'vitest';
import { add, multiply } from './helpers';

describe('Math Helpers', () => {
  describe('add', () => {
    it('should add two numbers correctly', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('should handle negative numbers', () => {
      expect(add(-5, 3)).toBe(-2);
    });

    it('should handle zero', () => {
      expect(add(0, 5)).toBe(5);
    });
  });

  describe('multiply', () => {
    it('should multiply two numbers correctly', () => {
      expect(multiply(3, 4)).toBe(12);
    });

    it('should return 0 when multiplying by 0', () => {
      expect(multiply(5, 0)).toBe(0);
    });
  });
});
```

### Component Testing

```tsx
// src/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button Component', () => {
  it('renders button with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button')).toHaveTextContent('Click me');
  });

  it('calls onClick handler when clicked', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();

    render(<Button onClick={handleClick}>Click</Button>);
    
    const button = screen.getByRole('button');
    await user.click(button);

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('renders disabled button', () => {
    render(<Button disabled>Disabled</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});
```

## Mocking

### Mocking Functions

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

describe('User Service', () => {
  let mockFetch: any;

  beforeEach(() => {
    mockFetch = vi.fn();
  });

  it('fetches user data', async () => {
    mockFetch.mockResolvedValue({
      json: async () => ({ id: 1, name: 'Alice' })
    });

    // Use mockFetch in your function
    const response = await mockFetch();
    const data = await response.json();

    expect(data).toEqual({ id: 1, name: 'Alice' });
    expect(mockFetch).toHaveBeenCalledTimes(1);
  });

  it('handles fetch error', async () => {
    mockFetch.mockRejectedValue(new Error('Network error'));

    await expect(mockFetch()).rejects.toThrow('Network error');
  });
});
```

### Mocking Modules

```ts
// src/services/api.test.ts
import { describe, it, expect, vi } from 'vitest';
import { fetchUsers } from './api';

// Mock the entire module
vi.mock('../lib/http', () => ({
  get: vi.fn(() => Promise.resolve([{ id: 1, name: 'Alice' }]))
}));

describe('API Service', () => {
  it('fetches users', async () => {
    const users = await fetchUsers();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe('Alice');
  });
});
```

### Partial Mocking

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

vi.mock('../utils', async () => {
  const actual = await vi.importActual('../utils');
  return {
    ...actual,
    dateUtil: {
      now: () => new Date('2025-01-01')
    }
  };
});
```

## Spying

```ts
import { describe, it, expect, vi, spyOn } from 'vitest';
import { logger } from './logger';

describe('Spy on function', () => {
  it('spies on console.log', () => {
    const consoleSpy = spyOn(console, 'log');

    console.log('test message');

    expect(consoleSpy).toHaveBeenCalledWith('test message');
    expect(consoleSpy).toHaveBeenCalledTimes(1);

    consoleSpy.mockRestore();
  });
});
```

## Async Testing

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

describe('Async Operations', () => {
  it('waits for promise to resolve', async () => {
    const fetchData = () =>
      new Promise(resolve => 
        setTimeout(() => resolve('data'), 100)
      );

    const data = await fetchData();
    expect(data).toBe('data');
  });

  it('handles async errors', async () => {
    const failingFetch = () =>
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Network error')), 100)
      );

    await expect(failingFetch()).rejects.toThrow('Network error');
  });
});
```

## Snapshot Testing

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

describe('Card Snapshot', () => {
  it('matches snapshot', () => {
    const { container } = render(
      <Card title="Test">Content</Card>
    );

    expect(container).toMatchSnapshot();
  });
});
```

## Code Coverage

### Run Coverage

```bash
npm run test:coverage
```

### Coverage Configuration

```ts
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      lines: 80,        // Minimum line coverage
      functions: 80,
      branches: 75,
      statements: 80,
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.d.ts',
        '**/*.test.ts'
      ]
    }
  }
});
```

## Vitest UI

```bash
npm run test:ui
# Opens interactive UI at http://localhost:51204/__vitest__/
```

## Performance Benefits

```
Framework | Time (500 tests)
----------|----------------
Jest      | ~8 seconds
Vitest    | ~3 seconds
Mocha     | ~6 seconds
```

Vitest is **~2.7x faster** than Jest!

## Anti-Patterns

```ts
// ❌ BAD: Slow synchronous operations
it('slow test', () => {
  for (let i = 0; i < 1000000; i++) {
    // Pointless loop
  }
});

// ❌ BAD: Global state pollution
let counter = 0;
it('increments counter', () => {
  counter++;
  expect(counter).toBe(1);
});
it('another test', () => {
  expect(counter).toBe(1);  // Depends on previous test!
});

// ✅ GOOD: Use beforeEach for setup
let counter: number;
beforeEach(() => {
  counter = 0;
});

// ❌ BAD: Testing implementation details
vi.spyOn(obj, 'privateMethod');

// ✅ GOOD: Test behavior, not implementation
expect(result).toBe(expectedValue);

// ❌ BAD: Snapshot without understanding
expect(largeObject).toMatchSnapshot();
// Snapshot bloat, hard to review

// ✅ GOOD: Targeted snapshots
expect({ name, email }).toMatchSnapshot();
```

## Verification Before Production

- [ ] Unit test coverage >= 80%
- [ ] No console warnings in tests
- [ ] Async tests properly handled
- [ ] Mocks cleaned up (mockRestore)
- [ ] No flaky tests (random failures)
- [ ] Tests isolated (no dependencies)
- [ ] Snapshot tests reviewed
- [ ] Coverage report passing

## Integration with Project Standards

Enforces T-1 through T-10:
- Colocated tests (same directory as source)
- Pure logic tested separately
- Edge cases covered
- Fast feedback (RED-GREEN-REFACTOR)

## Resources

- Vitest Docs: https://vitest.dev
- Testing Library: https://testing-library.com
- Vitest Examples: https://github.com/vitest-dev/vitest/tree/main/examples
---

**Last Updated:** January 24, 2026
**Compatibility:** Claude Opus 4.5, Claude Code v2.x
**Status:** Production Ready

> **January 2026 Update:** This skill is compatible with Claude Opus 4.5 and Claude Code v2.x. For complex tasks, use the `effort: high` parameter for thorough analysis.