Next.js Testing
Test Next.js apps with Jest, React Testing Library, and E2E testing with Playwright or Cypress.
TL;DR
- 01Use Jest for unit and integration tests of functions and components.
- 02Use React Testing Library to test components from the user perspective.
- 03Use Playwright or Cypress for end-to-end testing full user workflows.
Tips
- 01Aim for a test pyramid: many unit tests, some integration tests, and a few critical E2E tests to balance speed and confidence.
- 02Use Mock Service Worker (MSW) to intercept network requests in integration-style tests, since it mimics real API behavior more closely than manual <code>jest.mock</code> calls.
Warnings
- 01Avoid testing implementation details and focus on user behavior, since refactored code will break tests that depend on internal structure.
- 02E2E tests that wait on fixed timeouts instead of specific conditions or selectors become flaky, since real network and render timing varies between runs.
Setup and Jest Basics
- Next.js doesn't ship a test runner out of the box — install Jest and configure it with the built-in Next.js preset to get started.
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom - Create
jest.config.jsusing the built-in Next.js Jest preset.const nextJest = require("next/jest"); const createJestConfig = nextJest({ dir: "./" }); module.exports = createJestConfig({ testEnvironment: "jsdom" }); - Create test files with
.test.jsor.spec.jsextensions.// utils.test.js import { add } from "./utils"; it("adds two numbers", () => { expect(add(2, 3)).toBe(5); }); - Run tests in watch mode to re-run on file changes.
npm test -- --watch - Use
describeto group related tests anditfor individual test cases. Jest runs tests in parallel by default.
Testing Components
- Tests that check internal state break on every refactor — import React Testing Library to test components the way a user actually sees them.
import { render, screen } from "@testing-library/react"; import Button from "./Button"; it("renders a button", () => { render(<Button>Click me</Button>); const btn = screen.getByRole("button", { name: /click me/i }); expect(btn).toBeInTheDocument(); }); - Query elements using semantic methods like
getByRole,getByText.screen.getByRole("button", { name: /submit/i }); screen.getByLabelText("Email"); screen.getByText("Welcome"); - Test user interactions with
userEventinstead offireEvent.import userEvent from "@testing-library/user-event"; const user = userEvent.setup(); await user.click(button); - Avoid testing implementation details, focus on what users see and do.
Testing API Routes
- No server needed to test an API route — create separate test files that call route handlers directly with mocked request and response objects.
import { createMocks } from "node-mocks-http"; import handler from "./api/hello"; it("returns a greeting", async () => { const { req, res } = createMocks({ method: "GET" }); await handler(req, res); expect(res._getStatusCode()).toBe(200); expect(JSON.parse(res._getData())).toHaveProperty("message"); }); - Use a library like
node-mocks-httpto mock request and response. - Test different HTTP methods and status codes.
- Mock dependencies like databases or external APIs.
Mocking and Fixtures
- A flaky network call shouldn't fail your test suite — mock external API calls to keep tests fast and isolated.
jest.mock("./api", () => ({ fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: "Alice" })) })); - Use fixtures to provide consistent test data.
const mockUser = { id: 1, name: "Alice", email: "alice@example.com" }; it("displays user info", () => { render(<Profile user={mockUser} />); expect(screen.getByText("Alice")).toBeInTheDocument(); }); - Mock the App Router navigation module for components using
useRouterorusePathname.jest.mock("next/navigation", () => ({ useRouter: jest.fn(() => ({ push: jest.fn(), replace: jest.fn() })), usePathname: jest.fn(() => "/"), useSearchParams: jest.fn(() => new URLSearchParams()) })); - Mocking prevents external dependencies from slowing tests.
E2E Testing
- Unit tests can't catch a broken checkout flow across pages — use Playwright or Cypress for end-to-end testing of full workflows.
// playwright.spec.ts import { test, expect } from "@playwright/test"; test("user can sign up", async ({ page }) => { await page.goto("http://localhost:3000"); await page.fill("input[name=email]", "test@example.com"); await page.click("button:has-text('Sign Up')"); await expect(page).toHaveURL("/dashboard"); }); - E2E tests run in a real browser and test the entire application.
- Run E2E tests against a deployed environment before releasing.
- E2E tests are slower but catch integration issues that unit tests miss.
npx playwright test - Use E2E tests for critical user paths like checkout or login.
FAQ
Install jest, jest-environment-jsdom, @testing-library/react, and @testing-library/jest-dom, then add a jest.config.js using Next.js's built-in preset: const nextJest = require('next/jest'); module.exports = nextJest({ dir: './' })({testEnvironment: 'jsdom'}).
Both work well, but Playwright is generally preferred for new projects due to faster execution, built-in multi-browser support, and better handling of Next.js server components. Cypress has a more mature ecosystem and better real-time debugging.
Import the handler function directly and call it with mocked req/res objects using libraries like node-mocks-http, then assert on res.status() and res.json() values without spinning up a server.
This usually means the element is conditionally rendered or loaded asynchronously—use async queries like findByText or waitFor instead of getByText so the test waits for the DOM to update after data fetching or state changes.
Mock the next/navigation module (App Router) or next/router (Pages Router) using jest.mock: jest.mock('next/navigation', () => ({ useRouter: () => ({ push: jest.fn(), pathname: '/' }) })) at the top of your test file.