NextjsUnit Testing (Jest & Vitest)

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

Note
There isn't a wrong choice here. If your project already uses Vite tooling elsewhere, or you want faster watch-mode feedback, Vitest is a great fit. If you want the most battle-tested option with the largest amount of prior art, Jest is the safer default.
Setting up Jest

Bash
npm install --save-dev jest @testing-library/jest-dom @types/jest

TSX
// jest.config.ts
import nextJest from 'next/jest'

const createJestConfig = nextJest({ dir: './' })

export default createJestConfig({
  testEnvironment: 'jest-environment-jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
})

Bash
# package.json
"scripts": {
  "test": "jest --watch"
}
Setting up Vitest

Bash
npm install --save-dev vitest @vitejs/plugin-react jsdom

TSX
// 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:

TSX
// 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:

TSX
// 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.

Tip
Keep unit tests free of rendering and network calls entirely. The moment a "unit" test needs jsdom or a mocked fetch, it's usually a sign the logic under test should be extracted into its own pure function first, then tested on its own.