Vitest has replaced Jest as the go-to testing framework for modern JavaScript projects. Faster, ESM-native, and with a nearly identical API — the migration is low-friction and the performance gains are real.
Why Vitest Over Jest
- 10-20x faster: Runs tests in parallel using Vite's transform pipeline
- ESM-native: No transform config needed for modern JS
- In-source testing: Write tests in the same file as the code
-
Compatible API:
describe,it,expectwork the same as Jest -
Built-in coverage: No separate
@jest/coveragepackage
Setup
npm install -D vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // for React component tests
globals: true, // no need to import describe/it/expect
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
exclude: ['node_modules/', 'tests/', '*.config.*'],
},
},
})
Testing Pure Functions
// lib/pricing.test.ts
import { describe, it, expect } from 'vitest'
import { calculateDiscount, formatPrice } from './pricing'
describe('calculateDiscount', () => {
it('applies percentage discount', () => {
expect(calculateDiscount(100, { type: 'percent', value: 20 })).toBe(80)
})
it('applies fixed discount', () => {
expect(calculateDiscount(100, { type: 'fixed', value: 15 })).toBe(85)
})
it('does not go below zero', () => {
expect(calculateDiscount(10, { type: 'fixed', value: 50 })).toBe(0)
})
it('rejects negative discount values', () => {
expect(() => calculateDiscount(100, { type: 'percent', value: -5 })).toThrow()
})
})
Testing API Routes
// app/api/users/route.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { GET } from './route'
import { db } from '@/lib/db'
vi.mock('@/lib/db', () => ({
db: {
user: {
findMany: vi.fn(),
},
},
}))
vi.mock('next-auth', () => ({
getServerSession: vi.fn().mockResolvedValue({ user: { id: 'user-1' } }),
}))
describe('GET /api/users', () => {
beforeEach(() => vi.clearAllMocks())
it('returns users for authenticated request', async () => {
vi.mocked(db.user.findMany).mockResolvedValue([{ id: '1', name: 'Alice' }])
const req = new Request('http://localhost/api/users')
const res = await GET(req)
const body = await res.json()
expect(res.status).toBe(200)
expect(body).toHaveLength(1)
expect(body[0].name).toBe('Alice')
})
})
Testing React Components
// components/PricingCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { PricingCard } from './PricingCard'
describe('PricingCard', () => {
const defaultProps = {
name: 'Pro',
price: 99,
features: ['Feature 1', 'Feature 2'],
onSelect: vi.fn(),
}
it('renders plan name and price', () => {
render(<PricingCard {...defaultProps} />)
expect(screen.getByText('Pro')).toBeInTheDocument()
expect(screen.getByText('$99')).toBeInTheDocument()
})
it('calls onSelect when CTA is clicked', () => {
render(<PricingCard {...defaultProps} />)
fireEvent.click(screen.getByRole('button', { name: /get started/i }))
expect(defaultProps.onSelect).toHaveBeenCalledWith('Pro')
})
})
Snapshot Testing
it('matches snapshot', () => {
const result = formatUserData({ name: 'Alice', role: 'admin' })
expect(result).toMatchInlineSnapshot(`
{
"displayName": "Alice",
"permissions": ["read", "write", "delete"],
}
`)
})
Inline snapshots are stored in the test file itself — no separate __snapshots__ directory.
Coverage Enforcement
// package.json
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:ci": "vitest --coverage --reporter=junit"
}
}
// vitest.config.ts -- fail CI if coverage drops
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 70,
},
}
The Ship Fast Skill Pack at whoffagents.com includes a /test skill that generates Vitest test suites for any function, React component, or API route. $49 one-time.
Top comments (0)