Next.js Testing

Test Next.js apps with Vitest or Jest for components and Playwright for end-to-end flows.

TL;DR

  1. Unit test Client Components with vitest or jest and Testing Library.
  2. Cover async Server Components with end-to-end tests in Playwright.
  3. Run end-to-end tests against a production build made with next build.

Vitest Setup

    Install

    Add 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-paths
    vitest.config.mts

    Configure 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 script

    Add 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();
    });
    userEvent

    Simulate 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/jest

    Create 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-dom

    Add 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 Test

    Test 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 playwright

    Install Playwright and create its config file with one command.

    npm init playwright@latest
    Page Test

    Drive 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');
    });
    webServer

    Build 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

  1. Query elements by role and visible text with getByRole(), so tests match what a real user sees.
  2. Let Playwright start your app with the webServer option, so one command builds, serves, and tests it.

Warnings

  1. Vitest and Jest cannot render async Server Components yet, so test those pages with end-to-end tests instead.
  2. Mock next/navigation hooks such as useRouter() in unit tests, because no router exists outside a running app.

In Practice

FAQ