Developing Unit Tests for Frontend (Vitest)
Vitest—next-generation test runner from Vite team. Uses same config as Vite project, supports native ESM, order of magnitude faster than Jest on large projects. Default choice for Vite, Nuxt 3, SvelteKit projects.
Setup
npm install -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom
// vite.config.ts (or vitest.config.ts)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: { lines: 80, functions: 80, branches: 70 },
},
},
});
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => cleanup());
Syntax Identical to Jest
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { formatDate } from '../utils/date';
describe('formatDate', () => {
it('formats ISO date to English locale', () => {
const date = new Date('2024-11-15T10:00:00Z');
expect(formatDate(date, 'en-US')).toBe('November 15, 2024');
});
it('returns dash for null', () => {
expect(formatDate(null)).toBe('—');
});
});
Testing Components
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import userEvent from '@testing-library/user-event';
import { SearchInput } from './SearchInput';
describe('SearchInput', () => {
it('calls onSearch after debounce', async () => {
vi.useFakeTimers();
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} debounce={300} />);
await userEvent.type(screen.getByRole('searchbox'), 'laptop');
expect(onSearch).not.toHaveBeenCalled(); // debounce not passed
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledWith('laptop');
vi.useRealTimers();
});
it('clears input on Escape', async () => {
render(<SearchInput onSearch={vi.fn()} />);
const input = screen.getByRole('searchbox');
await userEvent.type(input, 'text');
await userEvent.keyboard('{Escape}');
expect(input).toHaveValue('');
});
});
Mocks and Spies
// vi.mock—replace module
vi.mock('../services/api', () => ({
getProducts: vi.fn().mockResolvedValue([
{ id: 1, name: 'MacBook', price: 1500 },
]),
}));
// vi.spyOn—spy on method
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
Concurrent Tests (Difference from Jest)
// Vitest supports parallel tests within describe
describe.concurrent('parallel tests', () => {
it.concurrent('test 1', async () => { ... });
it.concurrent('test 2', async () => { ... });
});
UI Mode
# Interactive browser UI
npx vitest --ui
# Opens http://localhost:51204/__vitest__/
# Shows test tree, coverage, failed test diffs
Performance vs Jest
On project with 500+ tests Vitest typically 2–5x faster than Jest due to:
- Native ESM without CJS transformation
- Shared dev server with main Vite process
- Watch mode with HMR for tests
Timeline
Setting up Vitest + migration from Jest (if needed) + writing first test suite: 2–4 days.







