Unit Testing (Jest & Vitest)
Unit tests check a single piece of logic in isolation — no rendering, no DOM, no network. They're the fastest tests you can write, and the best place to start covering business logic like formatters, calculations, and validation rules. Next.js officially supports both Jest and Vitest, and either works well in an App Router project.
Jest vs. Vitest
Jest | Vitest | |
|---|---|---|
Speed | Slower to start, mature caching | Very fast, native ESM, instant watch mode |
Config | next/jest handles most Next.js-specific setup automatically | Needs a small amount of manual Vite config, but plays natively with Vite-based tooling |
Ecosystem | Extremely mature, huge community, most tutorials assume it | Newer but rapidly adopted, especially in Vite-based projects |
API | describe/it/expect | describe/it/expect — nearly identical, largely a drop-in mental model |
Setting up Jest
npm install --save-dev jest @testing-library/jest-dom @types/jest
// jest.config.ts
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
export default createJestConfig({
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
})# package.json
"scripts": {
"test": "jest --watch"
}Setting up Vitest
npm install --save-dev vitest @vitejs/plugin-react jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: { environment: 'jsdom' },
})A worked example
Say you have a small utility that formats a price for display:
// lib/formatPrice.ts
export function formatPrice(cents: number, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(cents / 100)
}The test lives right next to it (or in a tests folder) and follows the same describe/it/expect structure in both Jest and Vitest:
// lib/formatPrice.test.ts
import { formatPrice } from './formatPrice'
describe('formatPrice', () => {
it('formats whole dollar amounts', () => {
expect(formatPrice(1000)).toBe('$10.00')
})
it('formats cents correctly', () => {
expect(formatPrice(1099)).toBe('$10.99')
})
it('supports a different currency', () => {
expect(formatPrice(1000, 'EUR')).toContain('10.00')
})
})describe groups related tests under a shared label, each it (or test) defines one specific case, and expect makes an assertion about the result. Run it once with npm test, or leave it in watch mode while you work — both runners re-run affected tests automatically as you edit the source file.
What belongs in a unit test
Pure functions — formatters, calculators, validators, parsers.
Logic with multiple branches or edge cases (empty input, zero, negative numbers, boundary values).
Anything you'd otherwise have to manually re-check by hand every time you touch it.
Not: rendering React components — that's Component Testing.
Not: full user flows across pages — that's End-to-End testing.