Vitest Pattern
Configure and use Vitest for fast, modern testing in Next.js applications with TypeScript, React Testing Library, and coverage reporting.
Overview#
Vitest is a blazing-fast test runner built on Vite, providing excellent TypeScript and ESM support out of the box. It's the recommended testing framework for modern Next.js applications.
When to use:
- Unit testing utility functions and classes
- Component testing with React Testing Library
- Testing Server Actions and API routes
- Testing custom hooks
Key features:
- Native ESM and TypeScript support
- Jest-compatible API
- Fast HMR-powered watch mode
- Built-in code coverage
- Snapshot testing
Code Example#
Setup Configuration#
1// vitest.config.ts
2import { defineConfig } from 'vitest/config'
3import react from '@vitejs/plugin-react'
4import path from 'path'
5
6export default defineConfig({
7 plugins: [react()],
8 test: {
9 environment: 'jsdom',
10 globals: true,
11 setupFiles: ['./vitest.setup.ts'],
12 include: ['**/*.test.{ts,tsx}'],
13 coverage: {
14 provider: 'v8',
15 reporter: ['text', 'json', 'html'],
16 exclude: ['node_modules/', '*.config.*']
17 }
18 },
19 resolve: {
20 alias: {
21 '@': path.resolve(__dirname, './src')
22 }
23 }
24})1// vitest.setup.ts
2import '@testing-library/jest-dom/vitest'
3import { cleanup } from '@testing-library/react'
4import { afterEach, vi } from 'vitest'
5
6afterEach(() => {
7 cleanup()
8})
9
10// Mock next/navigation
11vi.mock('next/navigation', () => ({
12 useRouter: () => ({
13 push: vi.fn(),
14 replace: vi.fn(),
15 back: vi.fn()
16 }),
17 usePathname: () => '/',
18 useSearchParams: () => new URLSearchParams()
19}))Unit Testing Functions#
1// lib/utils.test.ts
2import { describe, it, expect } from 'vitest'
3import { formatPrice, calculateDiscount, validateEmail } from './utils'
4
5describe('formatPrice', () => {
6 it('formats cents to dollars', () => {
7 expect(formatPrice(1000)).toBe('$10.00')
8 expect(formatPrice(1599)).toBe('$15.99')
9 })
10
11 it('handles zero', () => {
12 expect(formatPrice(0)).toBe('$0.00')
13 })
14})
15
16describe('calculateDiscount', () => {
17 it('applies percentage discount', () => {
18 expect(calculateDiscount(100, 10)).toBe(90)
19 expect(calculateDiscount(50, 50)).toBe(25)
20 })
21
22 it('returns original price for zero discount', () => {
23 expect(calculateDiscount(100, 0)).toBe(100)
24 })
25})
26
27describe('validateEmail', () => {
28 it('accepts valid emails', () => {
29 expect(validateEmail('user@example.com')).toBe(true)
30 expect(validateEmail('user+tag@domain.co.uk')).toBe(true)
31 })
32
33 it('rejects invalid emails', () => {
34 expect(validateEmail('invalid')).toBe(false)
35 expect(validateEmail('no@domain')).toBe(false)
36 expect(validateEmail('')).toBe(false)
37 })
38})Testing React Components#
1// components/button.test.tsx
2import { describe, it, expect, vi } from 'vitest'
3import { render, screen, fireEvent } from '@testing-library/react'
4import { Button } from './button'
5
6describe('Button', () => {
7 it('renders with children', () => {
8 render(<Button>Click me</Button>)
9 expect(screen.getByText('Click me')).toBeInTheDocument()
10 })
11
12 it('calls onClick when clicked', () => {
13 const handleClick = vi.fn()
14 render(<Button onClick={handleClick}>Click</Button>)
15
16 fireEvent.click(screen.getByText('Click'))
17 expect(handleClick).toHaveBeenCalledTimes(1)
18 })
19
20 it('is disabled when loading', () => {
21 render(<Button loading>Submit</Button>)
22 expect(screen.getByRole('button')).toBeDisabled()
23 })
24
25 it('applies variant classes', () => {
26 render(<Button variant="destructive">Delete</Button>)
27 expect(screen.getByRole('button')).toHaveClass('bg-red-500')
28 })
29})Testing Async Components#
1// components/user-profile.test.tsx
2import { describe, it, expect, vi } from 'vitest'
3import { render, screen, waitFor } from '@testing-library/react'
4import { UserProfile } from './user-profile'
5
6// Mock the fetch function
7vi.mock('@/lib/api', () => ({
8 fetchUser: vi.fn()
9}))
10
11import { fetchUser } from '@/lib/api'
12
13describe('UserProfile', () => {
14 it('shows loading state initially', () => {
15 vi.mocked(fetchUser).mockImplementation(() => new Promise(() => {}))
16
17 render(<UserProfile userId="123" />)
18 expect(screen.getByText('Loading...')).toBeInTheDocument()
19 })
20
21 it('displays user data after loading', async () => {
22 vi.mocked(fetchUser).mockResolvedValue({
23 name: 'John Doe',
24 email: 'john@example.com'
25 })
26
27 render(<UserProfile userId="123" />)
28
29 await waitFor(() => {
30 expect(screen.getByText('John Doe')).toBeInTheDocument()
31 expect(screen.getByText('john@example.com')).toBeInTheDocument()
32 })
33 })
34
35 it('shows error state on failure', async () => {
36 vi.mocked(fetchUser).mockRejectedValue(new Error('Failed'))
37
38 render(<UserProfile userId="123" />)
39
40 await waitFor(() => {
41 expect(screen.getByText('Error loading profile')).toBeInTheDocument()
42 })
43 })
44})Testing Server Actions#
1// actions/posts.test.ts
2import { describe, it, expect, vi, beforeEach } from 'vitest'
3import { createPost, deletePost } from './posts'
4import { prisma } from '@/lib/db'
5import { auth } from '@/lib/auth'
6
7vi.mock('@/lib/db', () => ({
8 prisma: {
9 post: {
10 create: vi.fn(),
11 delete: vi.fn(),
12 findUnique: vi.fn()
13 }
14 }
15}))
16
17vi.mock('@/lib/auth', () => ({
18 auth: vi.fn()
19}))
20
21vi.mock('next/cache', () => ({
22 revalidatePath: vi.fn()
23}))
24
25describe('createPost', () => {
26 beforeEach(() => {
27 vi.clearAllMocks()
28 })
29
30 it('creates post for authenticated user', async () => {
31 vi.mocked(auth).mockResolvedValue({ userId: 'user-123' })
32 vi.mocked(prisma.post.create).mockResolvedValue({
33 id: 'post-1',
34 title: 'Test Post',
35 authorId: 'user-123'
36 })
37
38 const formData = new FormData()
39 formData.set('title', 'Test Post')
40
41 const result = await createPost(formData)
42
43 expect(result.success).toBe(true)
44 expect(prisma.post.create).toHaveBeenCalledWith({
45 data: expect.objectContaining({
46 title: 'Test Post',
47 authorId: 'user-123'
48 })
49 })
50 })
51
52 it('returns error for unauthenticated user', async () => {
53 vi.mocked(auth).mockResolvedValue({ userId: null })
54
55 const formData = new FormData()
56 formData.set('title', 'Test')
57
58 const result = await createPost(formData)
59
60 expect(result.success).toBe(false)
61 expect(result.error).toBe('Not authenticated')
62 })
63})Testing Hooks#
1// hooks/use-debounce.test.ts
2import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3import { renderHook, act } from '@testing-library/react'
4import { useDebounce } from './use-debounce'
5
6describe('useDebounce', () => {
7 beforeEach(() => {
8 vi.useFakeTimers()
9 })
10
11 afterEach(() => {
12 vi.useRealTimers()
13 })
14
15 it('returns initial value immediately', () => {
16 const { result } = renderHook(() => useDebounce('hello', 500))
17 expect(result.current).toBe('hello')
18 })
19
20 it('debounces value changes', () => {
21 const { result, rerender } = renderHook(
22 ({ value }) => useDebounce(value, 500),
23 { initialProps: { value: 'hello' } }
24 )
25
26 rerender({ value: 'world' })
27 expect(result.current).toBe('hello') // Still old value
28
29 act(() => {
30 vi.advanceTimersByTime(500)
31 })
32
33 expect(result.current).toBe('world') // Now updated
34 })
35})Usage Instructions#
- Install dependencies:
npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom - Create config: Add
vitest.config.tswith your settings - Add setup file: Create
vitest.setup.tsfor global mocks and cleanup - Add scripts: Add
"test": "vitest"to package.json - Write tests: Create
*.test.tsor*.test.tsxfiles
Running Tests#
1# Run all tests
2npm test
3
4# Watch mode
5npm test -- --watch
6
7# Coverage report
8npm test -- --coverage
9
10# Run specific file
11npm test -- button.test.tsx
12
13# Run tests matching pattern
14npm test -- --grep "createPost"Best Practices#
- Use globals - Enable
globals: truefor cleaner test files - Mock external dependencies - Mock Next.js modules in setup
- Clean up after tests - Use
cleanup()in afterEach - Group related tests - Use
describeblocks for organization - Test behavior, not implementation - Focus on what components do
- Use async utilities - Use
waitForfor async operations - Keep tests fast - Mock slow dependencies like databases
Related Patterns#
- Unit Testing - Unit testing best practices
- Component Testing - React component testing
- Mocking - Mocking strategies
- Coverage - Code coverage setup