Testing Next.js Apps: Unit, Integration, and E2E
A complete guide to testing Next.js applications — from unit tests with Jest and React Testing Library to E2E tests with Playwright.

Testing a Next.js application requires different strategies depending on whether you are verifying isolated utility code, component interactions, or full end-to-end (E2E) user flows.
- Unit Testing: Validates standalone functions (like formatters or calculations) in isolation.
- Component Testing: Verifies that React components render and handle interactions correctly.
- End-to-End (E2E) Testing: Launches a browser to test full user scenarios (like completing a checkout flow) against a running server.
Configuring Jest and React Testing Library
To get started, install Jest, its Next.js wrapper, and Testing Library helpers:
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event ts-node
Create a setup script to initialize Testing Library matchers:
// jest.setup.ts
import '@testing-library/jest-dom';
Next, configure Jest using the built-in next/jest compiler mapping:
// jest.config.ts
import type { Config } from 'jest';
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
// Path to your Next.js app to load next.config.js and .env files
dir: './',
});
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
// Handle path aliases
'^@/(.*)$': '<rootDir>/$1',
},
};
export default createJestConfig(config);
Testing Server Components (Async rendering)
React Server Components are defined as async functions. Testing them requires invoking them and awaiting their resolution, then rendering the returned JSX structure.
// components/PostList.tsx
export interface Post {
id: string;
title: string;
}
export default async function PostList() {
const posts: Post[] = await fetch('https://api.example.com/posts')
.then((res) => {
if (!res.ok) throw new Error('API Error');
return res.json();
});
return (
<div>
<h2>Latest Posts</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Here is how you test this component by mocking the global fetch API:
// components/PostList.test.tsx
import { render, screen } from '@testing-library/react';
import PostList from './PostList';
// Mock global fetch
const mockFetch = jest.fn();
global.fetch = mockFetch;
describe('PostList Server Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders a list of posts fetched from the API', async () => {
// Configure mock resolution
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([
{ id: '1', title: 'Next.js Testing Best Practices' },
{ id: '2', title: 'React Server Components deep-dive' }
]),
});
// Render the resolved async JSX payload
const resolvedJSX = await PostList();
render(resolvedJSX);
expect(screen.getByText('Latest Posts')).toBeInTheDocument();
expect(screen.getByText('Next.js Testing Best Practices')).toBeInTheDocument();
expect(screen.getByText('React Server Components deep-dive')).toBeInTheDocument();
});
});
Mocking Navigation Hooks in Client Components
Many client components depend on routing hooks from next/navigation (like useRouter, usePathname, or useSearchParams). You can mock these hooks inside your test files:
// components/SearchBar.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export default function SearchBar() {
const router = useRouter();
const searchParams = useSearchParams();
const currentQuery = searchParams.get('q') || '';
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const q = data.get('query');
router.push(`/search?q=${q}`);
};
return (
<form onSubmit={handleSearch}>
<input defaultValue={currentQuery} name="query" placeholder="Search..." />
<button type="submit">Go</button>
</form>
);
}
// components/SearchBar.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchBar from './SearchBar';
const mockPush = jest.fn();
const mockGet = jest.fn();
// Mock next/navigation module
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
useSearchParams: () => ({
get: mockGet,
}),
}));
describe('SearchBar', () => {
it('navigates to the search route when submitted', async () => {
mockGet.mockReturnValue('previous-search');
render(<SearchBar />);
const input = screen.getByPlaceholderText('Search...');
const button = screen.getByRole('button', { name: 'Go' });
expect(input).toHaveValue('previous-search');
await userEvent.clear(input);
await userEvent.type(input, 'nextjs performance');
await userEvent.click(button);
expect(mockPush).toHaveBeenCalledWith('/search?q=nextjs performance');
});
});
End-to-End Testing with Playwright
Playwright runs tests in actual browsers (Chromium, Firefox, WebKit), making it suitable for verifying user-facing interactions.
Initialize Playwright in your project:
npm install --save-dev @playwright/test
npx playwright install
Configure Playwright to build and spin up your dev server automatically when running E2E tests:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
// Automatically spin up your local server before running tests
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Here is a sample end-to-end test verifying a basic login flow:
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test('redirects unauthorized users, then logs in successfully', async ({ page }) => {
// 1. Visit dashboard (should redirect to login)
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/);
// 2. Fill login form
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'securepassword123');
await page.click('button[type="submit"]');
// 3. Verify landing back on the dashboard
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('h1')).toContainText('Dashboard');
});
});
Run your tests using the command-line CLI:
- Run Jest tests:
npx jest - Run Playwright E2E tests:
npx playwright test
For how testing connects to the deployment pipeline, see deploying Next.js on Vercel. For patterns you'll want to test, see Server Actions.