| id | SK-056 |
| name | vitest-testing |
| description | Vitest — fast unit testing with Testing Library integration and test pyramid strategy |
| keywords | vitest, testing, unit-test, component-test, testing-library, mock, coverage, tdd, test-pyramid, react-testing |
| version | 1.0.0 |
Vitest Testing
When to Use This Skill
Apply when writing tests for any Vite-based or modern JS/TS project. Vitest is the 2026 default (6x faster than Jest, native ESM, shared Vite config). Test pyramid: Vitest for unit + component, Playwright for E2E. Auto-activate on keywords: vitest, test, describe, it, expect, mock, coverage.
Related skills: Playwright for E2E browser testing — via the anthropic-skills:playwright-cli skill or the Claude Browser / MCP_DOCKER browser tools (the old SK-009 Playwright skill is retired).
Installation
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Configuration
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}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: { lines: 80, branches: 80, functions: 80 },
},
css: true,
},
});
Setup file:
import '@testing-library/jest-dom/vitest';
Basic Test Structure (Arrange-Act-Assert)
import { describe, it, expect, beforeEach } from 'vitest';
describe('calculateTotal', () => {
it('returns sum of item prices', () => {
const items = [{ price: 10 }, { price: 20 }, { price: 30 }];
const total = calculateTotal(items);
expect(total).toBe(60);
});
it('returns 0 for empty array', () => {
expect(calculateTotal([])).toBe(0);
});
it('throws on negative prices', () => {
expect(() => calculateTotal([{ price: -5 }])).toThrow('Invalid price');
});
});
Component Testing with Testing Library
import { render, screen } 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('increments count on button click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('calls onChange callback with new value', async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render();
user.(screen.(, { : }));
(onChange).();
});
});
Mocking
Module mocks:
import { vi, describe, it, expect } from 'vitest';
vi.mock('./api', () => ({
fetchUser: vi.fn(),
}));
import { fetchUser } from './api';
describe('UserService', () => {
it('fetches and transforms user', async () => {
vi.mocked(fetchUser).mockResolvedValue({ id: '1', name: 'Alice' });
const result = await getUser('1');
expect(result.displayName).toBe('Alice');
});
});
Spy on methods:
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(spy).toHaveBeenCalledWith(expect.stringContaining('failed'));
spy.mockRestore();
Mock timers:
import { vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => { vi.useFakeTimers(); });
afterEach(() => { vi.useRealTimers(); });
it('debounces input', async () => {
const callback = vi.fn();
const debounced = debounce(callback, 300);
debounced('hello');
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledWith('hello');
});
Mock fetch:
import { vi } from 'vitest';
const mockFetch = vi.fn();
global.fetch = mockFetch;
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ id: '1', name: 'Alice' }),
});
Async Testing
it('resolves with user data', async () => {
const user = await fetchUser('1');
expect(user).toEqual({ id: '1', name: 'Alice' });
});
it('rejects on not found', async () => {
await expect(fetchUser('999')).rejects.toThrow('Not found');
});
import { waitFor } from '@testing-library/react';
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
Testing Hooks
import { renderHook, act } from '@testing-library/react';
it('increments counter', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
Testing with TanStack Query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
it('displays user data', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: () => ({ name: 'Alice' }) });
render(<UserProfile userId="1" />, { wrapper: createWrapper() });
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
Snapshot Testing
it('matches snapshot', () => {
const { container } = render(<Button variant="primary">Click</Button>);
expect(container.firstChild).toMatchSnapshot();
});
it('serializes config', () => {
expect(getConfig()).toMatchInlineSnapshot(`
{
"debug": false,
"port": 3000,
}
`);
});
Coverage
vitest run --coverage
vitest --coverage --watch
Test Pyramid Strategy
/ E2E \ Playwright — critical user flows (5-10 tests)
/----------\
/ Component \ Vitest + Testing Library — UI behavior (many)
/--------------\
/ Unit Tests \ Vitest — pure functions, utils, hooks (most)
/--------------------\
Rules:
- Unit tests: pure functions, utilities, hooks, reducers. Fast, isolated.
- Component tests: render + interact + assert. Mock external deps, not internal state.
- E2E tests: critical user paths only (auth flow, checkout, data CRUD). Slow, flaky risk.
- Test behavior, not implementation. Query by role/text, not class/id.
- One assertion cluster per test. Multiple related expects OK, but one logical check.
- Co-locate tests:
foo.ts → foo.test.ts in same directory.
CLI Commands
vitest
vitest run
vitest run src/utils
vitest -t "calculates"
vitest --reporter=verbose
vitest bench
Performance Tips
- Vitest runs tests in worker threads — parallel by default
- Use
--pool=threads (default) for speed, --pool=forks for isolation
--sequence.concurrent to run tests within a file concurrently
vi.mock is hoisted automatically — no need for manual hoisting
- Avoid
beforeAll database seeding — use per-test factories instead