vitest
Vitest
Blazing fast unit test framework powered by Vite with native TypeScript and ESM support.
Quick Start
Install:
npm install -D vitest
Add to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest --coverage"
}
}
Configure 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',
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
resolve: {
alias: {
'@': '/src',
},
},
});
Basic Testing
Test Structure
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
afterEach(() => {
// Cleanup
});
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract two numbers', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});
describe('division', () => {
it('should divide two numbers', () => {
expect(calculator.divide(6, 2)).toBe(3);
});
it('should throw on division by zero', () => {
expect(() => calculator.divide(6, 0)).toThrow('Division by zero');
});
});
});
Common Assertions
import { expect } from 'vitest';
// Equality
expect(value).toBe(5); // Strict equality
expect(value).toEqual({ a: 1 }); // Deep equality
expect(value).toStrictEqual({ a: 1 }); // Strict deep equality
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3, 5); // Floating point
// Strings
expect(value).toMatch(/pattern/);
expect(value).toContain('substring');
// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));
// Objects
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', 'value');
expect(object).toMatchObject({ partial: true });
// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('error message');
expect(() => fn()).toThrowError(/pattern/);
// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow('error');
// Negation
expect(value).not.toBe(5);
Async Testing
import { describe, it, expect, vi } from 'vitest';
describe('async operations', () => {
// Async/await
it('should fetch data', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1 });
});
// Returning promise
it('should resolve correctly', () => {
return expect(fetchData()).resolves.toEqual({ id: 1 });
});
// Using done callback
it('should call callback', (done) => {
fetchWithCallback((data) => {
expect(data).toBeDefined();
done();
});
});
// Testing rejected promises
it('should reject on error', async () => {
await expect(fetchInvalidData()).rejects.toThrow('Not found');
});
});
Mocking
Function Mocks
import { vi, describe, it, expect, beforeEach } from 'vitest';
describe('mocking functions', () => {
const mockFn = vi.fn();
beforeEach(() => {
mockFn.mockClear(); // Clear calls, keep implementation
// mockFn.mockReset(); // Clear everything
// mockFn.mockRestore(); // Restore original (for spies)
});
it('should track calls', () => {
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
});
it('should return mocked values', () => {
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
});
it('should mock implementation', () => {
mockFn.mockImplementation((x) => x * 2);
expect(mockFn(5)).toBe(10);
});
it('should mock resolved values', async () => {
mockFn.mockResolvedValue({ data: 'test' });
await expect(mockFn()).resolves.toEqual({ data: 'test' });
});
});
Module Mocks
import { vi, describe, it, expect } from 'vitest';
// Mock entire module
vi.mock('./database', () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
saveUser: vi.fn().mockResolvedValue(true),
}));
// Mock with factory
vi.mock('./api', () => {
return {
fetchPosts: vi.fn(() => Promise.resolve([])),
};
});
// Partial mock
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils');
return {
...actual,
formatDate: vi.fn(() => '2024-01-01'),
};
});
import { getUser } from './database';
import { fetchPosts } from './api';
describe('module mocking', () => {
it('should use mocked module', async () => {
const user = await getUser(1);
expect(user).toEqual({ id: 1, name: 'Test' });
});
});
Spies
import { vi, describe, it, expect } from 'vitest';
describe('spying', () => {
it('should spy on object method', () => {
const obj = {
method: (x: number) => x * 2,
};
const spy = vi.spyOn(obj, 'method');
obj.method(5);
expect(spy).toHaveBeenCalledWith(5);
expect(spy).toHaveReturnedWith(10);
spy.mockRestore();
});
it('should spy and mock', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
console.log('test');
expect(spy).toHaveBeenCalledWith('test');
spy.mockRestore();
});
});
Timers
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('timer mocking', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should advance timers', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
it('should run all timers', () => {
const callback = vi.fn();
setTimeout(callback, 100);
setTimeout(callback, 200);
vi.runAllTimers();
expect(callback).toHaveBeenCalledTimes(2);
});
it('should mock Date', () => {
vi.setSystemTime(new Date(2024, 0, 1));
expect(new Date().getFullYear()).toBe(2024);
});
});
React Testing
Setup
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
Component 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 { Counter } from './Counter';
describe('Counter', () => {
it('should render initial count', () => {
render(<Counter initialCount={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});
it('should increment on click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should call onChange when count changes', async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(<Counter initialCount={0} onChange={onChange} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(onChange).toHaveBeenCalledWith(1);
});
});
Testing Hooks
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should initialize with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should update when props change', () => {
const { result, rerender } = renderHook(
({ initial }) => useCounter(initial),
{ initialProps: { initial: 0 } }
);
expect(result.current.count).toBe(0);
rerender({ initial: 10 });
// Note: depends on hook implementation
});
});
Testing with Context
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ThemeProvider } from './ThemeContext';
import { ThemedButton } from './ThemedButton';
const renderWithTheme = (ui: React.ReactElement, theme = 'light') => {
return render(
<ThemeProvider initialTheme={theme}>{ui}</ThemeProvider>
);
};
describe('ThemedButton', () => {
it('should apply light theme styles', () => {
renderWithTheme(<ThemedButton>Click</ThemedButton>, 'light');
expect(screen.getByRole('button')).toHaveClass('theme-light');
});
it('should apply dark theme styles', () => {
renderWithTheme(<ThemedButton>Click</ThemedButton>, 'dark');
expect(screen.getByRole('button')).toHaveClass('theme-dark');
});
});
Testing Async Components
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserProfile } from './UserProfile';
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'John Doe' }),
}));
describe('UserProfile', () => {
it('should show loading state', () => {
render(<UserProfile userId="1" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should display user after loading', async () => {
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
it('should show error state', async () => {
const { fetchUser } = await import('./api');
vi.mocked(fetchUser).mockRejectedValueOnce(new Error('Failed'));
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('Error loading user')).toBeInTheDocument();
});
});
});
Snapshot Testing
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Card } from './Card';
describe('Card', () => {
it('should match snapshot', () => {
const { container } = render(
<Card title="Test" description="Description" />
);
expect(container).toMatchSnapshot();
});
it('should match inline snapshot', () => {
const { container } = render(<Card title="Test" />);
expect(container.innerHTML).toMatchInlineSnapshot(`
"<div class=\\"card\\"><h2>Test</h2></div>"
`);
});
});
Coverage
Install coverage provider:
npm install -D @vitest/coverage-v8
Configure:
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Run:
npm run test:coverage
Test Patterns
Test Utils
// test/utils.tsx
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export function renderWithProviders(
ui: React.ReactElement,
options = {}
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
options
);
}
export * from '@testing-library/react';
Data Builders
// test/factories.ts
interface User {
id: string;
name: string;
email: string;
}
export function createUser(overrides: Partial<User> = {}): User {
return {
id: '1',
name: 'Test User',
email: 'test@example.com',
...overrides,
};
}
// Usage
const user = createUser({ name: 'Custom Name' });
Best Practices
- One assertion focus - Each test verifies one behavior
- Descriptive names - Clear test descriptions
- Arrange-Act-Assert - Consistent test structure
- Avoid test interdependence - Tests run in isolation
- Mock external dependencies - Control test environment
Common Mistakes
| Mistake | Fix |
|---|---|
| Testing implementation | Test behavior and outcomes |
| Over-mocking | Only mock external dependencies |
| Brittle selectors | Use accessible queries |
| Missing cleanup | Use afterEach cleanup |
| Ignoring async | Always await async operations |
Reference Files
- references/patterns.md - Advanced test patterns
- references/mocking.md - Mocking strategies
- references/react.md - React testing patterns
More from mgd34msu/goodvibes-gemini
chakra-ui
Builds accessible React applications with Chakra UI v3 components, tokens, and recipes. Use when creating styled component systems, theming, or accessible form controls.
70fastify
Builds high-performance Node.js APIs with Fastify, TypeScript, schema validation, and plugins. Use when building fast REST APIs, microservices, or needing schema-based validation.
2code-smell-detector
Detects code smells, anti-patterns, and common bugs with quantified thresholds and severity scoring. Use when reviewing code quality, finding maintainability issues, detecting SOLID violations, or identifying technical debt.
2playwright
Tests web applications with Playwright including E2E tests, locators, assertions, and visual testing. Use when writing end-to-end tests, testing across browsers, automating user flows, or debugging test failures.
2vite
Builds web applications with Vite including dev server, production builds, plugins, and configuration. Use when scaffolding projects, configuring build tools, optimizing bundles, or setting up development environments.
2valibot
Validates data with Valibot's modular, tree-shakable schema library for minimal bundle size. Use when bundle size matters, building form validation, or needing lightweight TypeScript validation.
2