Skip to main content

Next.js Integration Testing

Integration testing is a crucial part of ensuring your Next.js application works correctly as a whole. While unit tests focus on individual components or functions, integration tests verify that multiple parts of your application work together as expected. In this guide, we'll explore how to implement effective integration tests for your Next.js applications.

What is Integration Testing?

Integration testing examines how different parts of your application interact with each other. In the context of Next.js, this typically means testing:

  • How multiple components work together on a page
  • How pages interact with data fetching methods
  • How navigation works between different routes
  • How the application interacts with backend services

Integration tests provide more confidence than unit tests as they verify that your application parts function correctly together, but they're faster and less complex to set up than end-to-end tests.

Setting Up the Testing Environment

Before we begin, make sure you have the necessary dependencies installed:

bash
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom

Next, create a Jest configuration file (jest.config.js) at the root of your project:

javascript
const nextJest = require('next/jest');

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files
dir: './',
});

// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);

Create a jest.setup.js file to add additional setup options:

javascript
// jest.setup.js
import '@testing-library/jest-dom';

Update your package.json to include a test script:

json
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}

Writing Integration Tests for Next.js Components

Let's look at a practical example. Imagine we have a simple blog that shows a list of posts and lets users click to see details:

Example: Testing Post List and Details Interaction

First, let's create the components we want to test:

jsx
// components/PostList.js
import Link from 'next/link';

export default function PostList({ posts }) {
return (
<div className="post-list">
<h2>Blog Posts</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link href={`/posts/${post.id}`}>
<a data-testid={`post-link-${post.id}`}>{post.title}</a>
</Link>
</li>
))}
</ul>
</div>
);
}
jsx
// pages/posts/[id].js
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

export default function PostDetail() {
const router = useRouter();
const { id } = router.query;
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
if (id) {
// In a real app, this would be an API call
fetch(`/api/posts/${id}`)
.then(res => res.json())
.then(data => {
setPost(data);
setLoading(false);
});
}
}, [id]);

if (loading) return <p>Loading...</p>;
if (!post) return <p>Post not found</p>;

return (
<div className="post-detail">
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}

Now, let's write an integration test that verifies these components work together:

jsx
// __tests__/post-integration.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { RouterContext } from 'next/dist/shared/lib/router-context';
import PostList from '../components/PostList';
import PostDetail from '../pages/posts/[id]';

// Mock the next/router
const createMockRouter = (props) => ({
query: {},
push: jest.fn(),
replace: jest.fn(),
prefetch: jest.fn().mockResolvedValue(undefined),
back: jest.fn(),
reload: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
...props,
});

// Mock fetch API
global.fetch = jest.fn();

describe('Blog post integration', () => {
const mockPosts = [
{ id: '1', title: 'First Post', content: 'This is the first post' },
{ id: '2', title: 'Second Post', content: 'This is the second post' },
];

beforeEach(() => {
fetch.mockClear();
});

it('displays posts and navigates to detail page', async () => {
// Set up fetch mock to return post data
fetch.mockResolvedValueOnce({
json: async () => mockPosts[0]
});

// Initial router setup for PostList
const mockRouter = createMockRouter({});

// Render the post list
render(
<RouterContext.Provider value={mockRouter}>
<PostList posts={mockPosts} />
</RouterContext.Provider>
);

// Check if posts are displayed
expect(screen.getByText('First Post')).toBeInTheDocument();
expect(screen.getByText('Second Post')).toBeInTheDocument();

// Mock navigation to post detail
const updatedRouter = createMockRouter({
query: { id: '1' },
});

// Render the post detail page
render(
<RouterContext.Provider value={updatedRouter}>
<PostDetail />
</RouterContext.Provider>
);

// Wait for the post to load
await waitFor(() => {
expect(screen.getByText('First Post')).toBeInTheDocument();
expect(screen.getByText('This is the first post')).toBeInTheDocument();
});
});
});

This integration test verifies that:

  1. The post list correctly displays all posts
  2. When a user navigates to a specific post, the correct post details are fetched and displayed

Testing Page Navigation

Next.js applications often involve navigation between pages. Let's test a navigation flow:

jsx
// __tests__/navigation-integration.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import { RouterContext } from 'next/dist/shared/lib/router-context';
import Home from '../pages/index';
import AboutPage from '../pages/about';

const createMockRouter = (props) => ({
query: {},
push: jest.fn(),
replace: jest.fn(),
prefetch: jest.fn().mockResolvedValue(undefined),
back: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
...props,
});

describe('Navigation integration', () => {
it('navigates from home to about page when about link is clicked', () => {
const router = createMockRouter({});

// Render the home page
render(
<RouterContext.Provider value={router}>
<Home />
</RouterContext.Provider>
);

// Find and click the About link
const aboutLink = screen.getByText('About');
fireEvent.click(aboutLink);

// Check if router.push was called with the correct path
expect(router.push).toHaveBeenCalledWith('/about', expect.anything(), expect.anything());

// Now render the About page to continue the flow
render(
<RouterContext.Provider value={createMockRouter({ asPath: '/about' })}>
<AboutPage />
</RouterContext.Provider>
);

// Verify About page content is displayed
expect(screen.getByRole('heading', { name: /about us/i })).toBeInTheDocument();
});
});

Testing Data Fetching

Integration tests for Next.js applications often need to test how components handle data fetching. Let's test a product listing page:

jsx
// __tests__/product-listing.integration.test.js
import { render, screen, waitFor } from '@testing-library/react';
import ProductsPage from '../pages/products';

// Mock the getServerSideProps function
jest.mock('../pages/products', () => {
const Original = jest.requireActual('../pages/products').default;
Original.getServerSideProps = jest.fn();
return Original;
});

import { getServerSideProps } from '../pages/products';

describe('Product Listing Integration', () => {
it('correctly displays products fetched from the API', async () => {
const mockProducts = [
{ id: 1, name: 'Product 1', price: 99.99 },
{ id: 2, name: 'Product 2', price: 149.99 }
];

// Mock the data returned by getServerSideProps
getServerSideProps.mockResolvedValueOnce({
props: {
products: mockProducts
}
});

// Get the props that would be passed to the page component
const { props } = await getServerSideProps({ req: {}, res: {} });

// Render the page with the props
render(<ProductsPage {...props} />);

// Verify products are displayed
expect(screen.getByText('Product 1')).toBeInTheDocument();
expect(screen.getByText('$99.99')).toBeInTheDocument();
expect(screen.getByText('Product 2')).toBeInTheDocument();
expect(screen.getByText('$149.99')).toBeInTheDocument();
});
});

Testing Form Submissions

Let's test a contact form integration with form validation and submission:

jsx
// __tests__/contact-form.integration.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ContactPage from '../pages/contact';

// Mock the API endpoint
global.fetch = jest.fn();

describe('Contact Form Integration', () => {
beforeEach(() => {
fetch.mockClear();
});

it('validates form fields and submits the form correctly', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true })
});

render(<ContactPage />);

// Fill out the form
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'John Doe' }
});

fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'invalid-email' }
});

// Submit with invalid email
fireEvent.click(screen.getByRole('button', { name: /submit/i }));

// Check if validation error appears
expect(screen.getByText(/please enter a valid email/i)).toBeInTheDocument();

// Correct the email
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: '[email protected]' }
});

fireEvent.change(screen.getByLabelText(/message/i), {
target: { value: 'This is a test message' }
});

// Submit the form with valid data
fireEvent.click(screen.getByRole('button', { name: /submit/i }));

// Check if fetch was called with the right data
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John Doe',
email: '[email protected]',
message: 'This is a test message'
})
});
});

// Check if success message is displayed
await waitFor(() => {
expect(screen.getByText(/thank you for your message/i)).toBeInTheDocument();
});
});
});

Testing API Routes

Next.js API routes can also be tested as part of integration tests:

jsx
// __tests__/api-route.integration.test.js
import { createMocks } from 'node-mocks-http';
import userHandler from '../pages/api/user/[id]';

describe('User API Integration', () => {
it('returns user data for valid ID', async () => {
const { req, res } = createMocks({
method: 'GET',
query: {
id: '1',
},
});

await userHandler(req, res);

expect(res._getStatusCode()).toBe(200);

const data = JSON.parse(res._getData());
expect(data).toEqual({
id: '1',
name: 'John Doe',
email: '[email protected]'
});
});

it('returns 404 for non-existent user', async () => {
const { req, res } = createMocks({
method: 'GET',
query: {
id: '999',
},
});

await userHandler(req, res);

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

Best Practices for Next.js Integration Tests

  1. Mock external services: Use jest.mock() to mock API calls, databases, or any external services
  2. Test realistic user flows: Design tests around common user journeys through your application
  3. Use data-testid attributes: Add data-testid attributes to elements that might be hard to select for tests
  4. Mock the Next.js router: Create a helper to mock the Next.js router for navigation-related tests
  5. Don't overdo it: Not everything needs an integration test; focus on key interactions and flows
  6. Keep tests independent: Each test should clean up after itself and not depend on other tests

Common Challenges and Solutions

Challenge: Testing Components with Server-Side Data

When testing Next.js pages that use getServerSideProps or getStaticProps, you need to mock these functions:

jsx
// Import the actual page component but mock the data fetching methods
jest.mock('../pages/products', () => {
const OriginalModule = jest.requireActual('../pages/products');
const MockedModule = {
__esModule: true,
default: OriginalModule.default,
getServerSideProps: jest.fn()
};
return MockedModule;
});

Challenge: Testing Dynamic Routes

When testing components that rely on dynamic routes, you need to carefully mock the router:

jsx
const router = createMockRouter({ query: { id: '123' }, asPath: '/products/123' });
render(
<RouterContext.Provider value={router}>
<ProductDetail />
</RouterContext.Provider>
);

Challenge: Handling Next.js Image Component

The Next.js Image component can be problematic in tests. You can create a mock for it:

jsx
// In your jest.setup.js file
jest.mock('next/image', () => ({
__esModule: true,
default: (props) => {
// eslint-disable-next-line jsx-a11y/alt-text
return <img {...props} />
},
}));

Summary

Integration testing in Next.js applications helps ensure that components, pages, API routes, and data fetching methods all work together correctly. By testing realistic user flows and interactions between different parts of your application, you can catch bugs that unit tests might miss while still keeping tests faster and simpler than full end-to-end tests.

In this guide, we covered:

  • Setting up the integration testing environment for Next.js
  • Testing component interactions and page navigation
  • Testing data fetching flows
  • Testing form validations and submissions
  • Testing API routes
  • Best practices and common challenges

Writing good integration tests requires finding the right balance between coverage and maintenance complexity. Focus on the critical paths in your application and the features most likely to break when components change.

Additional Resources

Exercises

  1. Write integration tests for a multi-step form wizard that collects user information across several screens
  2. Create tests for a product filtering system that lets users filter products by category and price
  3. Test a shopping cart implementation that lets users add/remove items and updates the total price
  4. Implement tests for a user authentication flow including login, registration, and protected routes
  5. Test a data dashboard that fetches and displays multiple data sources with loading states and error handling


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)