| name | Vitest Testing |
| description | This skill should be used when the user asks to "setup Vitest", "unit testing", "write tests", "mock functions", "vi.mock", "vi.fn", "vi.spyOn", or works with Vitest testing framework. |
| version | 0.1.0 |
Vitest Testing
Vitest is a next-generation testing framework powered by Vite, designed for speed and modern JavaScript/TypeScript projects.
Core Concepts
Test Types (Unit Tests Only)
- Unit Tests - Test isolated functions, classes, and components
- Component Tests - Test React/Vue components with Testing Library
Important: Vitest is for unit testing only. Use vi.mock, vi.fn, vi.spyOn for mocking. Integration/E2E tests should use Cypress without mocking.
Installation
npm install -D vitest @testing-library/react @testing-library/jest-dom
Project Structure
src/
├── components/
│ └── Button/
│ ├── Button.tsx
│ └── Button.test.tsx # Component test
├── api/
│ ├── client.ts
│ └── client.test.ts # API unit test
├── utils/
│ ├── index.ts
│ └── index.test.ts # Utility test
└── App.test.tsx # App component test
Configuration
vitest.config.ts:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'**/*.test.ts',
'**/*.config.*',
],
thresholds: {
lines: 70,
functions: 55,
branches: 55,
statements: 70,
},
},
},
})
setup.ts:
import '@testing-library/jest-dom/vitest'
Writing Tests
Basic Test Structure
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
describe('Feature Name', () => {
beforeEach(() => {
})
afterEach(() => {
})
it('should do something specific', () => {
const input = 'test'
const result = myFunction(input)
expect(result).toBe('expected')
})
})
Testing Pure Functions
import { describe, it, expect } from 'vitest'
import { sum, multiply } from './math'
describe('Math Utils', () => {
describe('sum', () => {
it('adds two positive numbers', () => {
expect(sum(1, 2)).toBe(3)
})
it('handles negative numbers', () => {
expect(sum(-1, 1)).toBe(0)
})
it('handles zero', () => {
expect(sum(0, 5)).toBe(5)
})
})
})
Mocking
vi.fn() - Create Mock Function
import { vi, describe, it, expect } from 'vitest'
describe('Callback Testing', () => {
it('calls callback with correct arguments', () => {
const callback = vi.fn()
processData('input', callback)
expect(callback).toHaveBeenCalledOnce()
expect(callback).toHaveBeenCalledWith('processed input')
})
it('tracks multiple calls', () => {
const callback = vi.fn()
callback('first')
callback('second')
expect(callback).toHaveBeenCalledTimes(2)
expect(callback.mock.calls).toEqual([['first'], ['second']])
})
})
vi.mock() - Mock Modules
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
import { fetchUser } from './api'
import * as userService from './userService'
vi.mock('./userService')
describe('User API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('returns user data', async () => {
vi.mocked(userService.getUser).mockResolvedValue({
id: 1,
name: 'John',
})
const result = await fetchUser(1)
expect(result.name).toBe('John')
expect(userService.getUser).toHaveBeenCalledWith(1)
})
it('handles errors', async () => {
vi.mocked(userService.getUser).mockRejectedValue(
new Error('User not found')
)
await expect(fetchUser(999)).rejects.toThrow('User not found')
})
})
vi.spyOn() - Spy on Methods
import { vi, describe, it, expect, afterEach } from 'vitest'
describe('Console Spy', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('spies on console.log', () => {
const spy = vi.spyOn(console, 'log')
myFunction()
expect(spy).toHaveBeenCalledWith('Expected message')
})
it('mocks return value', () => {
const spy = vi.spyOn(Math, 'random').mockReturnValue(0.5)
const result = getRandomNumber()
expect(result).toBe(50)
spy.mockRestore()
})
})
Mocking Fetch/API Calls
import { vi, describe, it, expect, beforeAll } from 'vitest'
describe('API Client', () => {
beforeAll(() => {
global.fetch = vi.fn()
})
it('fetches data successfully', async () => {
const mockData = { id: 1, name: 'Test' }
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: async () => mockData,
} as Response)
const result = await apiClient.get('/users/1')
expect(result).toEqual(mockData)
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/users/1'),
expect.any(Object)
)
})
it('handles API errors', async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: false,
status: 404,
json: async () => ({ message: 'Not found' }),
} as Response)
await expect(apiClient.get('/users/999')).rejects.toThrow('Not found')
})
})
Component Testing
Testing React Components
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/vitest'
import { Button } from './Button'
describe('Button Component', () => {
it('renders with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('handles click events', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(<Button onClick={onClick}>Click me</Button>)
await user.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalledOnce()
})
it('can be disabled', () => {
render(<Button disabled>Disabled</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})
Testing Async Components
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
import { UserProfile } from './UserProfile'
import * as api from './api'
vi.mock('./api')
describe('UserProfile Component', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('shows loading state', () => {
vi.mocked(api.fetchUser).mockImplementation(
() => new Promise(() => {})
)
render(<UserProfile userId={1} />)
expect(screen.getByText(/loading/i)).toBeInTheDocument()
})
it('displays user data on success', async () => {
vi.mocked(api.fetchUser).mockResolvedValue({
id: 1,
name: 'John Doe',
})
render(<UserProfile userId={1} />)
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
})
it('shows error message on failure', async () => {
vi.mocked(api.fetchUser).mockRejectedValue(new Error('Failed'))
render(<UserProfile userId={1} />)
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument()
})
})
})
Testing Timers
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
describe('Timer Functions', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('debounces function calls', () => {
const callback = vi.fn()
const debounced = debounce(callback, 100)
debounced()
debounced()
debounced()
expect(callback).not.toHaveBeenCalled()
vi.advanceTimersByTime(100)
expect(callback).toHaveBeenCalledOnce()
})
it('tests setTimeout', () => {
const callback = vi.fn()
setTimeout(callback, 1000)
vi.advanceTimersByTime(999)
expect(callback).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(callback).toHaveBeenCalledOnce()
})
})
Assertions Reference
expect(value).toBe(expected)
expect(value).toEqual(expected)
expect(value).toStrictEqual(expected)
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()
expect(value).toBeGreaterThan(n)
expect(value).toBeGreaterThanOrEqual(n)
expect(value).toBeLessThan(n)
expect(value).toBeLessThanOrEqual(n)
expect(value).toBeCloseTo(n, precision)
expect(string).toMatch(/regex/)
expect(string).toContain('substring')
expect(array).toContain(item)
expect(array).toHaveLength(n)
expect(object).toHaveProperty('key', value)
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('message')
expect(() => fn()).toThrow(ErrorType)
await expect(promise).resolves.toBe(value)
await expect(promise).rejects.toThrow('error')
expect(mock).toHaveBeenCalled()
expect(mock).toHaveBeenCalledOnce()
expect(mock).toHaveBeenCalledTimes(n)
expect(mock).toHaveBeenCalledWith(arg1, arg2)
expect(mock).toHaveBeenLastCalledWith(arg)
Running Tests
package.json scripts:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}
CLI commands:
npm test
npm run test:run
npm run test:coverage
npx vitest src/utils/index.test.ts
npx vitest -t "should handle errors"
Best Practices
- Follow AAA pattern - Arrange, Act, Assert
- One assertion focus - Each test should verify one behavior
- Use descriptive names - Test names should describe expected behavior
- Clean up mocks - Use
beforeEach/afterEach to clear/restore mocks
- Avoid implementation details - Test behavior, not internal state
- Mock at boundaries - Mock external dependencies (API, database)
- Keep tests isolated - Tests should not depend on each other
- Use
vi.mocked() - For TypeScript type safety with mocks
Testing Strategy
| Test Type | Tool | Mock Usage | Purpose |
|---|
| Unit tests | Vitest | ✅ Yes | Test isolated functions/components |
| Component tests | Vitest + RTL | ✅ Yes | Test UI components |
| E2E tests | Cypress | ❌ No | Test real integrations |
Common Patterns
Testing Error Boundaries
it('handles errors gracefully', async () => {
vi.mocked(api.getData).mockRejectedValue(new Error('Network error'))
render(<Component />)
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument()
})
expect(screen.queryByText(/data/i)).not.toBeInTheDocument()
})
Testing User Interactions
it('submits form on button click', async () => {
const user = userEvent.setup()
const onSubmit = vi.fn()
render(<Form onSubmit={onSubmit} />)
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' })
})