Next.js Testing
Test Next.js apps with Vitest or Jest for components and Playwright for end-to-end flows.
TL;DR
- Unit test Client Components with
vitestorjestand Testing Library. - Cover
asyncServer Components with end-to-end tests in Playwright. - Run end-to-end tests against a production build made with
next build.
Vitest Setup
InstallAdd Vitest, the React plugin, a browser-like environment, and Testing Library.
npm install -D vitest @vitejs/plugin-react jsdom \
@testing-library/react @testing-library/dom \
vite-tsconfig-pathsvitest.config.mtsConfigure the React plugin, path aliases, and the jsdom test environment.
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: { environment: 'jsdom' },
});test scriptAdd a script so the whole team runs tests the same way.
// package.json
{
"scripts": {
"test": "vitest"
}
}Component Tests
render()Render a component and find elements the way a user would.
import { expect, test } from 'vitest';
import {
render,
screen,
} from '@testing-library/react';
import Page from '../app/page';
test('shows the heading', () => {
render(<Page />);
const h1 = screen.getByRole('heading', { level: 1 });
expect(h1).toBeDefined();
});userEventSimulate real clicks and typing, then check what the screen shows.
import userEvent from '@testing-library/user-event';
test('counts clicks', async () => {
render(<Counter />);
await userEvent.click(screen.getByRole('button'));
expect(screen.getByText('Clicked 1')).toBeDefined();
});vi.mock()Replace next/navigation so components that call the router can render.
import { vi } from 'vitest';
const push = vi.fn();
vi.mock('next/navigation', () => ({
useRouter: () => ({ push }),
}));Jest Setup
next/jestCreate a Jest config that already understands Next.js, TypeScript, and CSS.
// jest.config.ts
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({ dir: './' });
export default createJestConfig({
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
});jest-domAdd readable DOM matchers such as toBeInTheDocument through the setup file.
// jest.setup.ts
import '@testing-library/jest-dom';
// in a test
expect(screen.getByRole('button')).toBeInTheDocument();Function TestTest plain helper functions directly, with no rendering at all.
import { formatPrice } from '@/lib/format';
test('formats cents as dollars', () => {
expect(formatPrice(1999)).toBe('$19.99');
});End-to-End With Playwright
npm init playwrightInstall Playwright and create its config file with one command.
npm init playwright@latestPage TestDrive a real browser through a user flow and assert on the result.
import { test, expect } from '@playwright/test';
test('navigates to about', async ({ page }) => {
await page.goto('/');
await page.getByText('About').click();
await expect(page).toHaveURL('/about');
});webServerBuild and start the app automatically before the tests run.
// playwright.config.ts
export default defineConfig({
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
use: { baseURL: 'http://localhost:3000' },
});Tips
- Query elements by role and visible text with
getByRole(), so tests match what a real user sees. - Let Playwright start your app with the
webServeroption, so one command builds, serves, and tests it.
Warnings
- Vitest and Jest cannot render
asyncServer Components yet, so test those pages with end-to-end tests instead. - Mock
next/navigationhooks such asuseRouter()in unit tests, because no router exists outside a running app.
In Practice
Test that a search form sends the user to the results page with their query.
- The mock replaces the router, so the component can call push in a test.
- userEvent types into the input the way a real person would.
- Pressing Enter submits the form through its normal handler.
- The assertion checks the navigation the user would experience.
import { expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchForm } from './SearchForm';
const push = vi.fn();
vi.mock('next/navigation', () => ({
useRouter: () => ({ push }),
}));
test('searches for the typed term', async () => {
render(<SearchForm />);
const input = screen.getByRole('searchbox');
await userEvent.type(input, 'shoes{Enter}');
expect(push).toHaveBeenCalledWith('/search?q=shoes');
});FAQ
Use two layers. Unit test Client Components and plain functions with Vitest or Jest plus React Testing Library. Use Playwright for end-to-end tests of full pages and user flows.
Not yet with Vitest or Jest. Both tools lack support for async components. Test the logic as plain functions and cover the rendered page with an end-to-end test.
Either works. Vitest is faster to set up and run. Jest has a built-in Next.js preset through next/jest. Pick one and use it across the project.
Mock the next/navigation module in your test file. Return an object with the methods you call, such as push, and assert that they were called.