home / skills / gruckion / marathon-ralph / setup-vitest

setup-vitest skill

/skills/setup-vitest

This skill configures Vitest for unit and integration tests, enabling seamless setup, coverage, and Testing Library integration.

npx playbooks add skill gruckion/marathon-ralph --skill setup-vitest

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.0 KB
---
name: setup-vitest
description: Configure Vitest for unit and integration testing. Use when setting up a test framework, when no test runner is detected, or when the user asks to configure testing.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
---

# Setup Vitest

Configure Vitest as the unit and integration test framework with Testing Library integration.

## When to Use This Skill

- No test framework is configured in the project
- User requests to set up unit testing
- Migrating from Jest to Vitest
- Setting up a new project that needs testing

## Installation

Use `ni` to auto-detect the package manager:

```bash
# Core Vitest packages
ni -D vitest @vitest/ui @vitest/coverage-v8

# For React projects
ni -D @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom

# For Vue projects
ni -D @testing-library/vue @testing-library/dom @testing-library/user-event @testing-library/jest-dom

# For Svelte projects
ni -D @testing-library/svelte @testing-library/dom @testing-library/user-event @testing-library/jest-dom
```

## Configuration

### vitest.config.ts

Create or update `vitest.config.ts` at the project root:

```typescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react' // For React projects

export default defineConfig({
  plugins: [react()], // Add framework plugin as needed
  test: {
    // Test file patterns
    include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
    exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'],

    // Environment - use 'jsdom' or 'happy-dom' for DOM testing
    environment: 'jsdom',

    // Enable global test APIs (describe, it, expect)
    globals: true,

    // Setup files run before each test file
    setupFiles: ['./tests/setup.ts'],

    // Mock behavior
    clearMocks: true,
    restoreMocks: true,

    // Coverage configuration
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      reportsDirectory: './coverage',
      include: ['src/**/*.{ts,tsx}'],
      exclude: [
        '**/*.test.{ts,tsx}',
        '**/*.spec.{ts,tsx}',
        '**/*.d.ts',
        '**/types/**',
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,
      },
    },

    // Timeouts
    testTimeout: 5000,
    hookTimeout: 10000,
  },
})
```

### TypeScript Configuration

Add Vitest types to `tsconfig.json`:

```json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}
```

### Setup File

Create `tests/setup.ts` for global test configuration:

```typescript
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'

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

// Mock window.matchMedia (common requirement)
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation((query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
})
```

### Package.json Scripts

Add test scripts to the **workspace** package.json (where the code lives):

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

## Monorepo Configuration

For monorepo projects (Turborepo, Nx, Lerna, etc.), additional setup is required.

### 1. Check Project State

Read `.claude/marathon-ralph.json` to get the project configuration:
- `project.monorepo.type` - The monorepo type (turbo, nx, lerna, etc.)
- `project.packageManager` - The package manager (bun, pnpm, yarn, npm)

### 2. Turborepo Setup

If using Turborepo (`turbo.json` exists), add the test task to the pipeline:

**turbo.json:**
```json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "test": {
      "dependsOn": ["^build"],
      "outputs": [],
      "cache": false
    },
    "test:run": {
      "dependsOn": ["^build"],
      "outputs": [],
      "cache": false
    }
  }
}
```

**Root package.json - add script to run tests across all workspaces:**
```json
{
  "scripts": {
    "test": "turbo run test",
    "test:run": "turbo run test:run"
  }
}
```

### 3. pnpm Workspaces Setup

For pnpm workspaces without Turborepo:

**Root package.json:**
```json
{
  "scripts": {
    "test": "pnpm -r test",
    "test:run": "pnpm -r test:run"
  }
}
```

### 4. npm/yarn Workspaces Setup

For npm or yarn workspaces:

**Root package.json:**
```json
{
  "scripts": {
    "test": "npm run test --workspaces",
    "test:run": "npm run test:run --workspaces"
  }
}
```

### 5. Workspace-Specific Testing

To run tests for a specific workspace, use the package manager's filter:

```bash
# Turborepo + bun
bun run --filter=web test

# pnpm
pnpm --filter web test

# npm workspaces
npm run test --workspace=web
```

## Writing Tests

### Query Priority (Most Accessible First)

Follow Testing Library's query priority:

1. **`getByRole`** - Best choice, tests accessibility
2. **`getByLabelText`** - For form fields
3. **`getByPlaceholderText`** - If no label available
4. **`getByText`** - For non-interactive elements
5. **`getByDisplayValue`** - For filled form values
6. **`getByAltText`** - For images
7. **`getByTitle`** - Rarely needed
8. **`getByTestId`** - Last resort only

### Example Test

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

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

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

    // Use accessible queries
    await user.type(screen.getByLabelText(/email/i), '[email protected]')
    await user.type(screen.getByLabelText(/password/i), 'password123')
    await user.click(screen.getByRole('button', { name: /sign in/i }))

    expect(onSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'password123',
    })
  })

  it('shows error for invalid email', async () => {
    const user = userEvent.setup()
    render(<LoginForm onSubmit={vi.fn()} />)

    await user.type(screen.getByLabelText(/email/i), 'invalid')
    await user.click(screen.getByRole('button', { name: /sign in/i }))

    expect(screen.getByRole('alert')).toHaveTextContent(/valid email/i)
  })
})
```

## Testing Philosophy

Follow Kent C. Dodds' testing principles:

### DO

- Test user behavior, not implementation details
- Use `screen` for all queries
- Prefer `getByRole` with accessible names
- Use `userEvent` over `fireEvent`
- Use `findBy*` for async elements
- Use `queryBy*` ONLY for asserting non-existence

### DON'T

- Test internal state or methods
- Use `container.querySelector`
- Use test IDs when better queries exist
- Add unnecessary accessibility attributes
- Mock everything (test real behavior where possible)

## Mocking

### Mock Functions

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

const mockFn = vi.fn()
mockFn.mockReturnValue('value')
mockFn.mockResolvedValue('async value')
```

### Mock Modules

```typescript
// Automatic mock
vi.mock('./api')

// Manual mock with factory
vi.mock('./api', () => ({
  fetchUser: vi.fn(() => ({ id: 1, name: 'Test' })),
}))

// Partial mock
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal()
  return {
    ...actual,
    specificFunction: vi.fn(),
  }
})
```

## Verification

After setup, verify with:

```bash
# Run tests
nr test

# Run with coverage
nr test:coverage

# Open UI mode
nr test:ui
```

## Directory Structure

```
project/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── Button.test.tsx    # Colocated tests
│   └── utils/
│       ├── helpers.ts
│       └── helpers.test.ts
├── tests/
│   └── setup.ts               # Global setup
├── vitest.config.ts
└── package.json
```

Overview

This skill configures Vitest as the project's unit and integration test framework with Testing Library integration and sensible defaults for DOM testing, coverage, and timeouts. It installs required packages, creates a vitest.config.ts, adds TypeScript types and setup files, and wires test scripts for single-repo and monorepo workflows. The goal is a repeatable, accessible testing baseline ready for React, Vue, or Svelte projects.

How this skill works

The skill installs Vitest and Testing Library packages, generates or updates vitest.config.ts with include/exclude patterns, environment, globals, mocks, coverage settings, and timeouts. It adds a tests/setup.ts file to initialize globals, cleanup, and common mocks (for example window.matchMedia) and updates tsconfig.json and package.json scripts. For monorepos, it adjusts root pipeline and workspace scripts for Turborepo, pnpm, npm/yarn workspaces to run tests across packages.

When to use it

  • Starting a new project that needs unit and integration tests.
  • No test runner is currently detected in the repository.
  • Migrating a codebase from Jest to Vitest.
  • Adding Testing Library (React/Vue/Svelte) integration for DOM tests.
  • Configuring CI or monorepo tooling to run tests across workspaces.

Best practices

  • Use Testing Library query priority: getByRole → getByLabelText → getByPlaceholderText → getByText, reserving getByTestId as last resort.
  • Prefer userEvent over fireEvent and test user behavior instead of implementation details.
  • Keep setup and mocks minimal in tests/setup.ts; mock only where necessary and restore/clear mocks between tests.
  • Use coverage provider 'v8' and set sensible thresholds; exclude test files and types from coverage.
  • Add workspace-level scripts (turbo/pnpm/npm) so CI can run tests across packages consistently.

Example use cases

  • Add Vitest to a React app: install @testing-library/react, configure vitest.config.ts with @vitejs/plugin-react and jsdom environment.
  • Migrate Jest tests to Vitest: add globals, update tsconfig types to include vitest/globals, and adapt mocks to vi.
  • Enable coverage reporting in CI: configure coverage.provider, reporters, and thresholds and run vitest run --coverage.
  • Set up monorepo testing: add test pipeline entries to turbo.json and root scripts to run tests across workspaces.
  • Create accessible component tests: use screen and getByRole with userEvent to simulate real user interactions.

FAQ

Which environment should I choose for DOM tests?

Use 'jsdom' for a complete DOM environment; use 'happy-dom' for faster, lighter DOM simulations if full compatibility isn't required.

How do I run tests across a monorepo?

Add workspace-level scripts (turbo.json pipeline or root package.json scripts) and use your package manager filter (turbo/pnpm/npm) to run tests per workspace or across all workspaces.