Next.js Testing

Test Next.js apps with Jest, React Testing Library, and E2E testing with Playwright or Cypress.

TL;DR

  1. 01Use Jest for unit and integration tests of functions and components.
  2. 02Use React Testing Library to test components from the user perspective.
  3. 03Use Playwright or Cypress for end-to-end testing full user workflows.

Tips

  1. 01Aim for a test pyramid: many unit tests, some integration tests, and a few critical E2E tests to balance speed and confidence.
  2. 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

  1. 01Avoid testing implementation details and focus on user behavior, since refactored code will break tests that depend on internal structure.
  2. 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.js using 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.js or .spec.js extensions.
    // 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 describe to group related tests and it for 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 userEvent instead of fireEvent.
    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-http to 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 useRouter or usePathname.
    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