Skip to main content
skill CX Codex

vitest-testing-patterns — Claude Code skill: how to install

Provides Vitest and React Testing Library conventions, mock patterns for APIs and databases, and directory rules for unit and component testing.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add curiositech/some_claude_skills --skill vitest-testing-patterns -a claude-code
Open on GitHub

What vitest-testing-patterns does

The skill standardizes test directories inside src/app/api/__tests__/, src/components/__tests__/, and src/lib/__tests__/. Test files use .test.ts or .test.tsx extensions. The setup relies on vitest.config.ts, a jsdom environment, src/test/setup.ts, and the v8 coverage provider.

It includes code examples for testing Next.js route handlers and isolating database calls. Component recipes pair @testing-library/react with userEvent. Clocks freeze via vi.useFakeTimers. There is also a stub pattern for @anthropic-ai/sdk.

These patterns do not cover end-to-end testing in a real browser. Use Playwright for that. Running npm test directly requires execution permissions for npm and npx in the environment.

What it helps with

  • write API route tests with mocked database calls and auth sessions
  • verify React component behaviors using userEvent and waitFor
  • isolate third-party SDKs using vi.mock
  • test helper functions with frozen system time

Example prompts for the agent

  • Write a component test for FeatureComponent checking the loading spinner and submission handler
  • Create unit tests for the GET endpoint in app/api/feature with a mocked auth session
  • Test the formatDate utility by freezing the system clock with vi.useFakeTimers

How to install vitest-testing-patterns

1Skills CLI (Vercel)

npx skills add curiositech/some_claude_skills --skill vitest-testing-patterns -a claude-code

2Manually: put the files in place

~/.claude/skills/vitest-testing-patterns/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/vitest-testing-patterns/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about vitest-testing-patterns

How to install vitest-testing-patterns in Claude Code?

Run npx skills add curiositech/some_claude_skills --skill vitest-testing-patterns -a claude-code in your terminal.

What does vitest-testing-patterns do?

It supplies patterns and conventions for writing Vitest unit tests, React Testing Library component suites, and mock setups for external dependencies.

Does vitest-testing-patterns support end-to-end testing?

No, it is scoped to unit and integration testing. E2E workflows require dedicated tools like Playwright or Cypress.

Source text

SKILL.md · MIT
---
name: vitest-testing-patterns
description: Write tests using Vitest and React Testing Library. Use when creating unit tests, component tests, integration tests, or mocking dependencies. Activates for test file creation, mock patterns,
  coverage, and testing best practices.
allowed-tools: Read,Write,Edit,Bash(npm:*,npx:*)
metadata:
  category: Code Quality & Testing
  tags:
  - testing
  - code
  - automation
  - jest
  - react
  pairs-with:
  - skill: test-automation-expert
    reason: Vitest unit testing is one layer in a comprehensive test automation strategy
  - skill: playwright-e2e-tester
    reason: Unit tests (Vitest) and E2E tests (Playwright) form complementary test pyramid layers
  - skill: react-performance-optimizer
    reason: Component test patterns verify that performance optimizations preserve correct behavior
  - skill: typescript-advanced-patterns
    reason: Type-safe test utilities and mock factories leverage advanced TypeScript patterns
---

# Vitest Testing Patterns

This skill helps you write effective tests using Vitest and React Testing Library following project conventions.

## When to Use

✅ **USE this skill for:**
- Writing unit tests for utilities and functions
- Creating component tests with React Testing Library
- Setting up mocks for API calls, databases, or external services
- Integration testing patterns
- Understanding test coverage and CI setup

❌ **DO NOT use for:**
- Jest-specific patterns → similar but check Jest docs for differences
- End-to-end testing → use Playwright or Cypress skills
- Performance testing → use dedicated performance tools
- API contract testing → use OpenAPI/Pact patterns

## Test Infrastructure

**Configuration**: `vitest.config.ts`
- Environment: jsdom
- Setup file: `src/test/setup.ts`
- Coverage: v8 provider

**Commands**:
```bash
npm test              # Watch mode
npm run test:run      # Single run
npm run test:coverage # With coverage
```

## File Organization

```
src/
├── app/api/__tests__/        # API route tests
├── components/__tests__/     # Component tests
├── lib/__tests__/            # Library/utility tests
└── lib/{feature}/__tests__/  # Feature-specific tests
```

Name tests as `{name}.test.ts` or `{name}.test.tsx`.

## Core Testing Patterns

### 1. API Route Tests

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST } from '../route';
import { NextRequest } from 'next/server';

// Mock dependencies
vi.mock('@/lib/auth', () => ({
  getSession: vi.fn(),
}));

vi.mock('@/db', () => ({
  db: {
    select: vi.fn().mockReturnThis(),
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue([]),
  },
}));

describe('GET /api/feature', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('returns 401 when not authenticated', async () => {
    vi.mocked(getSession).mockResolvedValue(null);

    const request = new NextRequest('http://localhost/api/feature');
    const response = await GET(request);

    expect(response.status).toBe(401);
  });

  it('returns data when authenticated', async () => {
    vi.mocked(getSession).mockResolvedValue({ userId: 'user-123' });
    vi.mocked(db.select).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
      }),
    });

    const request = new NextRequest('http://localhost/api/feature');
    const response = await GET(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveLength(1);
  });
});
```

### 2. Component Tests

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

// Mock hooks
vi.mock('@/hooks/useAuth', () => ({
  useAuth: vi.fn().mockReturnValue({
    user: { id: 'user-123', name: 'Test User' },
    isLoading: false,
  }),
}));

describe('FeatureComponent', () => {
  it('renders loading state', () => {
    vi.mocked(useAuth).mockReturnValueOnce({
      user: null,
      isLoading: true,
    });

    render(<FeatureComponent />);
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('handles user interaction', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

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

    await user.type(screen.getByRole('textbox'), 'Test input');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(onSubmit).toHaveBeenCalledWith('Test input');
  });

  it('displays error state', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));

    render(<FeatureComponent />);

    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
    });
  });
});
```

### 3. Library/Utility Tests

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { processData, formatDate } from '../utils';

describe('processData', () => {
  it('transforms input correctly', () => {
    const input = { raw: 'data' };
    const result = processData(input);

    expect(result).toEqual({
      processed: true,
      data: 'DATA',
    });
  });

  it('throws on invalid input', () => {
    expect(() => processData(null)).toThrow('Invalid input');
  });
});

describe('formatDate', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2025-01-15T10:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('formats relative dates', () => {
    const yesterday = new Date('2025-01-14T10:00:00Z');
    expect(formatDate(yesterday)).toBe('yesterday');
  });
});
```

## Mocking Patterns

### Module Mocking

```typescript
// Mock entire module
vi.mock('@/lib/auth', () => ({
  getSession: vi.fn(),
  requireAuth: vi.fn(),
}));

// Mock with partial implementation
vi.mock('date-fns', async () => {
  const actual = await vi.importActual('date-fns');
  return {
    ...actual,
    format: vi.fn(() => '2025-01-15'),
  };
});

// Mock default export (like Anthropic SDK)
vi.mock('@anthropic-ai/sdk', () => ({
  default: class MockAnthropic {
    messages = {
      create: vi.fn().mockResolvedValue({
        content: [{ type: 'text', text: 'Mock response' }],
        usage: { input_tokens: 10, output_tokens: 20 },
      }),
    };
  },
}));
```

### Function Mocking

```typescript
// Create mock function
const mockFn = vi.fn();

// Set return values
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('Failed'));

// One-time behavior
mockFn.mockReturnValueOnce('first call only');

// Custom implementation
mockFn.mockImplementation((arg) => arg.toUpperCase());

// Verify calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('expected', 'args');
```

### Chained Mock Pattern (Drizzle ORM)

```typescript
vi.mock('@/db', () => ({
  db: {
    select: vi.fn().mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          orderBy: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([{ id: '1' }]),
          }),
        }),
      }),
    }),
    insert: vi.fn().mockReturnValue({
      values: vi.fn().mockReturnValue({
        returning: vi.fn().mockResolvedValue([{ id: 'new-1' }]),
      }),
    }),
  },
}));
```

### Timer Mocking

```typescript
describe('debounced function', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('debounces calls', async () => {
    const callback = vi.fn();
    const debounced = debounce(callback, 300);

    debounced();
    debounced();
    debounced();

    expect(callback).not.toHaveBeenCalled();

    vi.advanceTimersByTime(300);

    expect(callback).toHaveBeenCalledTimes(1);
  });
});
```

## Query Priorities

Use queries in this order (most to least preferred):

1. **getByRole** - Accessible queries (buttons, links, headings)
2. **getByLabelText** - Form fields with labels
3. **getByPlaceholderText** - Inputs with placeholders
4. **getByText** - Non-interactive elements
5. **getByTestId** - Last resort (data-testid)

```typescript
// Preferred
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByLabelText(/email/i);

// Avoid unless necessary
screen.getByTestId('submit-button');
```

## Async Patterns

```typescript
// Wait for element to appear
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeInTheDocument();
});

// Find (built-in waitFor)
const element = await screen.findByText('Loaded');

// Wait for element to disappear
await waitFor(() => {
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
```

## Test Cleanup

```typescript
import { cleanup } from '@testing-library/react';

afterEach(() => {
  cleanup();            // React cleanup (automatic with setup.ts)
  vi.clearAllMocks();   // Reset mock call counts
  vi.resetAllMocks();   // Reset mocks to initial state
  vi.restoreAllMocks(); // Restore original implementations
});
```

## Accessibility Testing

```typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<Component />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
```

## Common Matchers

```typescript
// jest-dom matchers (from setup.ts)
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toHaveTextContent('text');
expect(element).toHaveAttribute('href', '/path');
expect(element).toHaveClass('active');
expect(input).toHaveValue('input value');
```

## References

- [Vitest Mocking Guide](https://vitest.dev/guide/mocking)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
- [Testing Library Queries](https://testing-library.com/docs/queries/about)

© curiositech · MIT · curiositech/some_claude_skills

Similar skills

All skills for Testing and QA →

From the same set23

ai-video-production-master A skill that runs the whole script-to-video pipeline on Apple Silicon Macs: stock footage, T2V and I2V generation, cloud GPU batching. Useful when you need a finished clip, not a single frame. CX Codex +1 398installs indie-monetization-strategist Guides indie developers and solo founders through choosing monetization models, freemium tiers, and sponsorship rates. Useful when turning a side project into a business. CX Codex +1 387installs adhd-daily-planner A planning skill built for ADHD brains: realistic time estimates, buffers between tasks, and a three-item daily plan. CX Codex +1 378installs voice-audio-engineer A skill that wires ElevenLabs into your agent for TTS, voice cloning, podcast production, transcription and voice UI work. CX Codex +1 367installs sound-engineer Gives the agent working knowledge of spatial audio, procedural sound effects and UI sound for games and apps. CX Codex +1 348installs geospatial-data-pipeline Processes drone imagery, optimizes GeoJSON payloads, and configures spatial indexes in PostGIS. Useful when building location services and map tiles. CX Codex +1 345installs photo-content-recognition-curation-expert A skill for recognizing what is in your photos: face clustering, near-duplicate detection, burst selection and screenshot filtering. Useful when a large library needs sorting. CX Codex +1 330installs admin-dashboard Extends an admin dashboard, developer portal and operations console with new tabs, metrics, monitoring and user management. CX Codex +1 323installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor