Vitest logo

vitest

Modern testing framework powered by Vite

$ npx docs2skills add vitest
SKILL.md

Vitest

Modern testing framework powered by Vite

What this skill does

Vitest is a fast testing framework that leverages Vite's transformation pipeline for lightning-quick test execution. It provides a Jest-compatible API while offering superior performance through native ESM support and smart caching. Developers choose Vitest for its seamless integration with Vite projects, unified configuration, and instant hot module replacement in watch mode.

Unlike traditional test runners that require complex setup and slow transpilation, Vitest reuses your existing Vite configuration including plugins, aliases, and transformations. This eliminates configuration drift between development and testing environments while providing features like parallel test execution, coverage reporting, and advanced mocking capabilities.

Prerequisites

  • Node.js >=20.0.0
  • Vite >=6.0.0
  • Package manager (npm, yarn, pnpm, or bun)
  • TypeScript support (optional but recommended)

Quick start

npm install -D vitest

Create a simple function and test:

// sum.js
export function sum(a, b) {
  return a + b
}

// sum.test.js
import { expect, test } from 'vitest'
import { sum } from './sum.js'

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

Add test script to package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "coverage": "vitest run --coverage"
  }
}

Run tests: npm test

Core concepts

Test Discovery: Files matching *.test.{js,ts} or *.spec.{js,ts} patterns are automatically discovered.

Watch Mode: Default behavior that reruns tests when files change, leveraging Vite's HMR for instant feedback.

Unified Configuration: Shares Vite's config including plugins, aliases, and transformations, eliminating setup duplication.

Test Context: Each test runs in isolated context with access to global APIs like expect, describe, it, beforeEach.

Concurrent Execution: Tests run in parallel by default for maximum performance.

Key API surface

// Test definition
test('description', () => {}) // alias: it()
describe('suite', () => {})

// Lifecycle hooks
beforeEach(() => {})
afterEach(() => {})
beforeAll(() => {})
afterAll(() => {})

// Assertions
expect(value).toBe(expected)
expect(value).toEqual(expected)
expect(fn).toThrow()
expect(fn).toHaveBeenCalled()

// Async testing
test('async', async () => {})
await expect(promise).resolves.toBe(value)
await expect(promise).rejects.toThrow()

// Mocking
vi.mock('./module')
vi.spyOn(object, 'method')
vi.fn() // mock function
vi.resetAllMocks()

// Test utilities
test.skip('skipped test', () => {})
test.only('focused test', () => {})
test.concurrent('parallel test', async () => {})

Common patterns

Basic unit testing:

import { describe, it, expect } from 'vitest'
import { Calculator } from './calculator'

describe('Calculator', () => {
  it('should add numbers correctly', () => {
    const calc = new Calculator()
    expect(calc.add(2, 3)).toBe(5)
  })

  it('should handle edge cases', () => {
    const calc = new Calculator()
    expect(calc.divide(5, 0)).toBe(Infinity)
  })
})

Mocking modules:

import { vi, expect, test } from 'vitest'
import { fetchUser } from './api'
import { getUserProfile } from './user-service'

vi.mock('./api')

test('getUserProfile calls fetchUser', async () => {
  const mockUser = { id: 1, name: 'John' }
  vi.mocked(fetchUser).mockResolvedValue(mockUser)
  
  const result = await getUserProfile(1)
  expect(fetchUser).toHaveBeenCalledWith(1)
  expect(result).toEqual(mockUser)
})

Testing with setup/teardown:

import { beforeEach, afterEach, describe, it, expect } from 'vitest'
import { setupDatabase, cleanupDatabase, createUser } from './test-utils'

describe('User service', () => {
  beforeEach(async () => {
    await setupDatabase()
  })

  afterEach(async () => {
    await cleanupDatabase()
  })

  it('should create user', async () => {
    const user = await createUser({ name: 'Test' })
    expect(user.id).toBeDefined()
  })
})

Snapshot testing:

import { test, expect } from 'vitest'
import { render } from './render-utils'

test('component renders correctly', () => {
  const result = render('<Button>Click me</Button>')
  expect(result).toMatchSnapshot()
})

Testing errors:

import { test, expect } from 'vitest'
import { validateEmail } from './validation'

test('validates email format', () => {
  expect(() => validateEmail('invalid')).toThrow('Invalid email format')
  expect(validateEmail('test@example.com')).toBe(true)
})

Configuration

vitest.config.ts:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,           // Use global APIs without imports
    environment: 'jsdom',    // or 'happy-dom', 'node'
    setupFiles: ['./setup.ts'],
    coverage: {
      provider: 'v8',        // or 'istanbul'
      reporter: ['text', 'html', 'lcov']
    },
    testTimeout: 10000,
    watch: true,
    reporters: ['verbose']
  }
})

Shared Vite config:

/// <reference types="vitest/config" />
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue()],
  test: {
    // test config here
  }
})

Environment variables:

VITEST_SKIP_INSTALL_CHECKS=1  # Skip dependency prompts
VITEST_POOL_SIZE=4           # Limit concurrent workers

Best practices

  • Use describe blocks to group related tests and provide context
  • Write descriptive test names that explain the expected behavior
  • Follow AAA pattern: Arrange, Act, Assert
  • Mock external dependencies to isolate units under test
  • Use beforeEach for test setup instead of duplicating code
  • Leverage TypeScript for better test maintenance and IDE support
  • Use test.concurrent for independent async tests to improve performance
  • Organize tests in same directory structure as source code
  • Use expect.assertions() for async tests to ensure all assertions run
  • Prefer toEqual for object comparison over toBe

Test organization patterns

Co-located tests:

src/
  components/
    Button.vue
    Button.test.ts
  utils/
    helpers.ts
    helpers.test.ts

Separate test directory:

src/
  components/
    Button.vue
  utils/
    helpers.ts
tests/
  components/
    Button.test.ts
  utils/
    helpers.test.ts

Gotchas and common mistakes

  • File naming: Tests must include .test. or .spec. in filename or they won't be discovered
  • Bun compatibility: Use bun run test instead of bun test to avoid Bun's built-in test runner
  • Mock hoisting: vi.mock() calls are hoisted, define them at module top level
  • Async cleanup: Always await async operations in afterEach hooks
  • Global pollution: Mocks persist between tests unless explicitly reset with vi.resetAllMocks()
  • Environment variables: Changes to process.env in tests affect other tests unless restored
  • Timer mocks: Remember to call vi.useRealTimers() after using vi.useFakeTimers()
  • Module mocking order: Import statements are hoisted above vi.mock(), use dynamic imports if needed
  • Coverage exclusions: Files without tests still count toward coverage unless excluded
  • Config precedence: vitest.config.ts overrides vite.config.ts, use mergeConfig for inheritance
  • Watch mode sensitivity: Large projects may hit file system limits, configure test.watchIgnore
  • Circular dependencies: Can cause infinite loops in module mocking
  • DOM environment: Browser APIs require environment: 'jsdom' or 'happy-dom'