vitest
Modern testing framework powered by Vite
$ npx docs2skills add vitestVitest
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
describeblocks 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
beforeEachfor test setup instead of duplicating code - Leverage TypeScript for better test maintenance and IDE support
- Use
test.concurrentfor 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
toEqualfor object comparison overtoBe
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 testinstead ofbun testto 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
afterEachhooks - Global pollution: Mocks persist between tests unless explicitly reset with
vi.resetAllMocks() - Environment variables: Changes to
process.envin tests affect other tests unless restored - Timer mocks: Remember to call
vi.useRealTimers()after usingvi.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.tsoverridesvite.config.ts, usemergeConfigfor 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'