Next.js pages and API routes are built on Node.js, and Jest is a fantastic tool for testing JavaScript. The trick is bridging the gap between a running Next.js application and Jest’s execution environment.

Here’s a basic setup for testing a Next.js page that fetches data, and an API route that provides it.

Project Structure:

my-next-app/
├── pages/
│   ├── api/
│   │   └── users.js
│   └── index.js
├── __tests__/
│   ├── api/
│   │   └── users.test.js
│   └── index.test.js
└── package.json

pages/api/users.js:

// This API route simulates fetching user data.
export default function handler(req, res) {
  if (req.method === 'GET') {
    const users = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ];
    res.status(200).json(users);
  } else {
    res.setHeader('Allow', ['GET']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

pages/index.js:

import { useState, useEffect } from 'react';

function HomePage({ initialUsers }) {
  const [users, setUsers] = useState(initialUsers);

  useEffect(() => {
    // Fetch users on mount if initialUsers is not provided (client-side)
    if (!initialUsers) {
      fetch('/api/users')
        .then((res) => res.json())
        .then((data) => setUsers(data));
    }
  }, [initialUsers]);

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

export async function getServerSideProps() {
  // Fetch users on the server for SSR
  const res = await fetch('http://localhost:3000/api/users'); // Use actual or mock URL
  const initialUsers = await res.json();
  return { props: { initialUsers } };
}

export default HomePage;

__tests__/api/users.test.js:

import { createMocks } from 'node-mocks-http';
import usersHandler from '../../pages/api/users';

describe('/api/users API Endpoint', () => {
  it('should return a list of users on GET request', async () => {
    const { req, res } = createMocks({
      method: 'GET',
    });

    await usersHandler(req, res);

    expect(res._getStatusCode()).toBe(200);
    expect(JSON.parse(res._getData())).toEqual([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  });

  it('should return 405 for unsupported methods', async () => {
    const { req, res } = createMocks({
      method: 'POST',
    });

    await usersHandler(req, res);

    expect(res._getStatusCode()).toBe(405);
  });
});

__tests__/index.test.js:

import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import Home from '../pages/index';

// Mock the fetch API for client-side rendering tests
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]),
  })
);

describe('Home Page', () => {
  it('renders the heading', () => {
    render(<Home initialUsers={[]} />); // Provide empty initialUsers to trigger client-side fetch
    expect(screen.getByRole('heading', { name: /users/i })).toBeInTheDocument();
  });

  it('renders user list from initialProps', () => {
    const initialUsers = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ];
    render(<Home initialUsers={initialUsers} />);
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });

  it('fetches and renders users on client-side', async () => {
    render(<Home initialUsers={undefined} />); // undefined initialUsers to trigger useEffect fetch

    // Wait for the fetch to complete and the UI to update
    await screen.findByText('Alice');
    await screen.findByText('Bob');

    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });
});

package.json:

{
  "name": "my-next-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest --watchAll"
  },
  "dependencies": {
    "next": "13.4.1",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@testing-library/react-hooks": "^8.0.4",
    "jest": "^29.5.0",
    "jest-environment-jsdom": "^29.5.0",
    "node-mocks-http": "^3.2.0"
  }
}

Setup:

  1. Install Dependencies:
    npm install next react react-dom
    npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/react-hooks jest jest-environment-jsdom node-mocks-http
    
  2. Configure Jest: Create a jest.config.js file in your project root:
    /** @type {import('jest').Config} */
    const config = {
      testEnvironment: 'jest-environment-jsdom',
      setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
      moduleNameMapper: {
        // Handle CSS imports (if you have them)
        '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
        // Handle static assets (if you have them)
        '\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',
      },
    };
    
    module.exports = config;
    
  3. Create jest.setup.js:
    import '@testing-library/jest-dom';
    
  4. Create __mocks__/fileMock.js: (If you have static assets)
    module.exports = 'test-file-stub';
    

Running Tests:

npm test

Testing API Routes:

  • node-mocks-http: This library is crucial. It provides mock req (request) and res (response) objects that mimic Node.js’s HTTP objects. This allows you to call your API route handlers directly within Jest, without needing to start a full server.
  • createMocks(): This function from node-mocks-http generates the mock request and response objects. You pass in the method, and can add other properties like query, body, etc., to simulate different incoming requests.
  • Assertion: You then call your API handler function with these mocks and assert on the properties of the mock res object (like _getStatusCode() and _getData()).

Testing Pages:

  • @testing-library/react: This is the standard for testing React components. render mounts your component in a simulated DOM environment (provided by jest-environment-jsdom).
  • screen: Provides query methods (like getByRole, getByText) to find elements within your rendered component.
  • getServerSideProps and getStaticProps:
    • For getServerSideProps, you often want to test the result of the props it provides. The Home component receives initialUsers. You can test the component directly with mock initialUsers to ensure it renders correctly.
    • If you need to test the getServerSideProps function itself, you would typically mock the fetch call within it and assert on the returned props.
  • Client-side Fetching (useEffect):
    • global.fetch = jest.fn(...): You can globally mock the fetch API. This is essential because your component might make network requests. You control what fetch returns, allowing you to simulate successful responses, errors, etc.
    • screen.findBy...: When your component updates its state based on an asynchronous operation (like fetch), you need to wait for those updates. findBy... queries are asynchronous and will wait until the element appears or a timeout occurs.
  • initialUsers={undefined} vs initialUsers={[]}: In the index.test.js example:
    • render(<Home initialUsers={undefined} />): This scenario simulates a component being rendered without server-side rendered props. The useEffect hook will run, and the test will wait for fetch to complete and render the data.
    • render(<Home initialUsers={initialUsers} />): This simulates server-side rendering where initialUsers are directly passed as props. The useEffect hook’s condition !initialUsers would be false, so it wouldn’t attempt to fetch.

The most surprising thing about this setup is how seamlessly Jest, with the help of libraries like node-mocks-http and @testing-library/react, can simulate the entire Next.js request/response lifecycle and component rendering within a pure JavaScript testing environment. You’re not actually running a Next.js server for these tests; you’re simulating its behavior.

This approach allows you to test your API logic and your page components in isolation, ensuring they behave as expected under various conditions without the overhead of a full application startup. You can test edge cases for your API routes (e.g., invalid input, different HTTP methods) and verify that your components correctly display data fetched both server-side and client-side.

The next logical step is to explore testing Next.js dynamic routes and more complex data fetching patterns like SWR or React Query.

Want structured learning?

Take the full Jest course →