vitest
Vitest Testing Framework
A blazing fast unit testing framework powered by Vite.
Quick Reference
| Command | Purpose |
|---|---|
vitest |
Run in watch mode |
vitest run |
Run once and exit |
vitest --coverage |
Generate coverage |
vitest --ui |
Open UI dashboard |
vitest --reporter=verbose |
Verbose output |
Why Vitest?
- Native ESM support - No configuration needed
- Vite-powered - Instant HMR for tests
- Jest-compatible - Familiar API, easy migration
- TypeScript first - Built-in TypeScript support
- Fast - Significantly faster than Jest
1. Setup
Installation
# Basic installation
npm install -D vitest
# With testing library
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
# With coverage
npm install -D @vitest/coverage-v8
Configuration (vitest.config.ts)
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
// Test environment
environment: 'jsdom', // or 'node', 'happy-dom'
// Global test APIs (describe, it, expect)
globals: true,
// Setup files
setupFiles: ['./src/test/setup.ts'],
// Include patterns
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
// Coverage
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/'],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
// Reporters
reporters: ['default', 'html'],
// Timeout
testTimeout: 10000,
// CSS handling
css: true,
// Module resolution
alias: {
'@': '/src',
},
},
});
Setup File (src/test/setup.ts)
import '@testing-library/jest-dom';
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Global mocks
vi.mock('./analytics', () => ({
track: vi.fn(),
}));
2. Basic Tests
Test Structure
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
describe('Calculator', () => {
beforeEach(() => {
// Setup
});
afterEach(() => {
vi.clearAllMocks();
});
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('handles edge cases', () => {
expect(add(0, 0)).toBe(0);
expect(add(-1, 1)).toBe(0);
});
// Skip test
it.skip('skipped test', () => {});
// Only run this test
it.only('only this test', () => {});
// Todo test
it.todo('implement this later');
// Concurrent tests
it.concurrent('runs in parallel', async () => {
const result = await fetchData();
expect(result).toBeDefined();
});
});
Matchers (Jest-compatible)
// Equality
expect(value).toBe(expected);
expect(value).toEqual(expected);
expect(value).toStrictEqual(expected);
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3, 5);
// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');
// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);
// Objects
expect(obj).toHaveProperty('key', 'value');
expect(obj).toMatchObject({ partial: true });
// Exceptions
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrow('message');
expect(() => throwError()).toThrowError(ErrorClass);
// Snapshots
expect(value).toMatchSnapshot();
expect(value).toMatchInlineSnapshot();
Parameterized Tests
import { describe, it, expect } from 'vitest';
describe('Math operations', () => {
it.each([
[1, 1, 2],
[2, 2, 4],
[3, 3, 6],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
// With object syntax
it.each([
{ a: 1, b: 1, expected: 2 },
{ a: 2, b: 2, expected: 4 },
])('add($a, $b) = $expected', ({ a, b, expected }) => {
expect(add(a, b)).toBe(expected);
});
});
3. Mocking
Mock Functions
import { vi } from 'vitest';
// Create mock function
const mockFn = vi.fn();
const mockWithReturn = vi.fn().mockReturnValue(42);
const mockAsync = vi.fn().mockResolvedValue({ data: 'test' });
// Implementations
mockFn.mockReturnValue(10);
mockFn.mockReturnValueOnce(5);
mockFn.mockResolvedValue({ data: 'async' });
mockFn.mockRejectedValue(new Error('failed'));
mockFn.mockImplementation((x) => x * 2);
// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('last');
Mock Modules
import { vi } from 'vitest';
// Mock module
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1 }),
}));
// Mock with factory
vi.mock('./database', async () => {
const actual = await vi.importActual('./database');
return {
...actual,
connect: vi.fn(),
};
});
// Auto-mock all exports
vi.mock('./utils');
// Reset mocks
afterEach(() => {
vi.resetAllMocks();
vi.restoreAllMocks();
});
Spies
import { vi } from 'vitest';
const obj = {
method: (x: number) => x * 2,
};
// Spy on method
const spy = vi.spyOn(obj, 'method');
obj.method(5);
expect(spy).toHaveBeenCalledWith(5);
expect(spy).toHaveReturnedWith(10);
// Spy with mock implementation
vi.spyOn(obj, 'method').mockReturnValue(100);
Timer Mocks
import { vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('handles timers', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
it('runs all timers', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
vi.runAllTimers();
expect(callback).toHaveBeenCalledTimes(2);
});
4. Async Testing
// Async/await
it('fetches data', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1 });
});
// Promises
it('resolves with data', () => {
return expect(fetchData()).resolves.toEqual({ id: 1 });
});
it('rejects with error', () => {
return expect(failingFetch()).rejects.toThrow('Network error');
});
// With assertions count
it('makes multiple assertions', async () => {
expect.assertions(2);
const data = await fetchData();
expect(data.id).toBe(1);
expect(data.name).toBe('John');
});
5. React Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('handles click', async () => {
const onClick = vi.fn();
const user = userEvent.setup();
render(<Button onClick={onClick}>Click</Button>);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('button')).toHaveTextContent('Loading...');
});
});
describe('Form', () => {
it('submits with valid data', async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
});
6. Vue Testing
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.vue';
describe('Counter', () => {
it('increments count', async () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain('0');
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('1');
});
it('accepts initial value', () => {
const wrapper = mount(Counter, {
props: { initialCount: 10 },
});
expect(wrapper.text()).toContain('10');
});
});
7. Snapshot Testing
import { expect, it } from 'vitest';
import { render } from '@testing-library/react';
it('matches snapshot', () => {
const { container } = render(<Button>Click</Button>);
expect(container).toMatchSnapshot();
});
it('matches inline snapshot', () => {
const user = getUser(1);
expect(user).toMatchInlineSnapshot(`
{
"id": 1,
"name": "John",
}
`);
});
// Update snapshots: vitest -u
8. Coverage
# Run with coverage
vitest run --coverage
# With specific reporter
vitest run --coverage --coverage.reporter=lcov
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage',
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
},
});
9. CI/CD Integration
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx vitest run --coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
10. Migration from Jest
Config Changes
// jest.config.js -> vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // Use global describe/it/expect
environment: 'jsdom',
setupFiles: ['./jest.setup.ts'], // Reuse setup
},
});
API Differences
// Jest -> Vitest
jest.fn() -> vi.fn()
jest.mock() -> vi.mock()
jest.spyOn() -> vi.spyOn()
jest.useFakeTimers() -> vi.useFakeTimers()
jest.clearAllMocks() -> vi.clearAllMocks()
// Import vi in files (or set globals: true)
import { vi } from 'vitest';
Best Practices
- Use globals: true - Cleaner test files
- Leverage HMR - Keep vitest running in watch mode
- Parallel by default - Tests run concurrently
- Use happy-dom - Faster than jsdom for most cases
- Inline snapshots - Better for small outputs
- Type-safe mocks - Leverage TypeScript
- Fast setup - Keep setup files minimal
- Coverage thresholds - Enforce quality
- Browser mode - For accurate DOM testing
- UI mode - vitest --ui for visual debugging
More from housegarofalo/claude-code-base
mqtt-iot
Configure MQTT brokers (Mosquitto, EMQX) for IoT messaging, device communication, and smart home integration. Manage topics, QoS levels, authentication, and bridging. Use when setting up IoT messaging, smart home communication, or device-to-cloud connectivity. (project)
22devops-engineer-agent
Infrastructure and DevOps specialist. Manages Docker, Kubernetes, CI/CD pipelines, and cloud deployments. Expert in GitHub Actions, Azure DevOps, Terraform, and container orchestration. Use for deployment automation, infrastructure setup, or CI/CD optimization.
6postgresql
Design, optimize, and manage PostgreSQL databases. Covers indexing, pgvector for AI embeddings, JSON operations, full-text search, and query optimization. Use when working with PostgreSQL, database design, or building data-intensive applications.
6home-assistant
Ultimate Home Assistant skill - complete administration, wireless protocols (Zigbee/ZHA/Z2M, Z-Wave JS, Thread, Matter), ESPHome device building, advanced troubleshooting, performance optimization, security hardening, custom integration development, and professional dashboard design. Covers configuration, REST API, automation debugging, database optimization, SSL/TLS, Jinja2 templating, and HACS custom cards. Use for any HA task.
6testing
Comprehensive testing skill covering unit, integration, and E2E testing with pytest, Jest, Cypress, and Playwright. Use for writing tests, improving coverage, debugging test failures, and setting up testing infrastructure.
5react-typescript
Build modern React applications with TypeScript. Covers React 18+ patterns, hooks, component architecture, state management (Zustand, Redux Toolkit), server components, and best practices. Use for React development, TypeScript integration, component design, and frontend architecture.
5