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:
- 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 - Configure Jest: Create a
jest.config.jsfile 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; - Create
jest.setup.js:import '@testing-library/jest-dom'; - 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 mockreq(request) andres(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 fromnode-mocks-httpgenerates the mock request and response objects. You pass in themethod, and can add other properties likequery,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
resobject (like_getStatusCode()and_getData()).
Testing Pages:
@testing-library/react: This is the standard for testing React components.rendermounts your component in a simulated DOM environment (provided byjest-environment-jsdom).screen: Provides query methods (likegetByRole,getByText) to find elements within your rendered component.getServerSidePropsandgetStaticProps:- For
getServerSideProps, you often want to test the result of the props it provides. TheHomecomponent receivesinitialUsers. You can test the component directly with mockinitialUsersto ensure it renders correctly. - If you need to test the
getServerSidePropsfunction itself, you would typically mock thefetchcall within it and assert on the returned props.
- For
- Client-side Fetching (
useEffect):global.fetch = jest.fn(...): You can globally mock thefetchAPI. This is essential because your component might make network requests. You control whatfetchreturns, allowing you to simulate successful responses, errors, etc.screen.findBy...: When your component updates its state based on an asynchronous operation (likefetch), you need to wait for those updates.findBy...queries are asynchronous and will wait until the element appears or a timeout occurs.
initialUsers={undefined}vsinitialUsers={[]}: In theindex.test.jsexample:render(<Home initialUsers={undefined} />): This scenario simulates a component being rendered without server-side rendered props. TheuseEffecthook will run, and the test will wait forfetchto complete and render the data.render(<Home initialUsers={initialUsers} />): This simulates server-side rendering whereinitialUsersare directly passed as props. TheuseEffecthook’s condition!initialUserswould 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.