Next.js Mocking
When testing Next.js applications, one of the most important techniques you'll need to master is mocking. Mocking allows you to replace real implementations of services, APIs, or components with simplified versions that make testing easier, faster, and more reliable.
What is Mocking?
Mocking is a technique where you replace real objects with fake (mock) objects that simulate the behavior of the real ones. In the context of testing Next.js applications, you might want to mock:
- External API calls
- Database operations
- Next.js router
- Authentication services
- Environment variables
- Server-side functionality
Why Mock in Next.js Tests?
- Speed: Tests run faster without making actual API or database calls
- Reliability: Tests don't fail because of external service issues
- Control: You can simulate different responses, including edge cases
- Isolation: You test only the component or function you intend to test
Setting Up for Mocking
To get started with mocking in Next.js, you'll need a testing framework like Jest, which is typically used with Next.js. Let's set up the basic testing environment:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
Then, create or update your jest.config.js
file:
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
module.exports = createJestConfig(customJestConfig)
And create a jest.setup.js
file:
import '@testing-library/jest-dom'
Mocking Techniques in Next.js
1. Mocking API Requests with Jest
Let's say you have a component that fetches user data from an API:
// components/UserProfile.js
import { useEffect, useState } from 'react'
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`)
const userData = await response.json()
setUser(userData)
} catch (error) {
console.error('Failed to fetch user:', error)
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId])
if (loading) return <p>Loading user data...</p>
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
)
}
To test this component, you can mock the fetch
function:
// components/UserProfile.test.js
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
import UserProfile from './UserProfile'
// Mock the global fetch function
global.fetch = jest.fn()
describe('UserProfile', () => {
beforeEach(() => {
// Clear mock between tests
jest.clearAllMocks()
})
test('renders user data when fetch is successful', async () => {
// Set up the mock to return a successful response
global.fetch.mockResolvedValueOnce({
json: jest.fn().mockResolvedValueOnce({
name: 'John Doe',
email: '[email protected]'
})
})
render(<UserProfile userId="123" />)
// Check if loading state is shown
expect(screen.getByText('Loading user data...')).toBeInTheDocument()
// Wait for the loading state to be removed
await waitForElementToBeRemoved(() => screen.queryByText('Loading user data...'))
// Check if user data is rendered
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Email: [email protected]')).toBeInTheDocument()
// Verify that fetch was called correctly
expect(global.fetch).toHaveBeenCalledWith('/api/users/123')
})
})
2. Mocking API Requests with MSW (Mock Service Worker)
For more complex API mocking, Mock Service Worker (MSW) provides a more robust solution:
First, install MSW:
npm install --save-dev msw
Now, let's create API mocks:
// mocks/handlers.js
import { rest } from 'msw'
export const handlers = [
rest.get('/api/users/:userId', (req, res, ctx) => {
const { userId } = req.params
// Simulate different responses based on userId
if (userId === '123') {
return res(
ctx.status(200),
ctx.json({
name: 'John Doe',
email: '[email protected]'
})
)
}
return res(
ctx.status(404),
ctx.json({ message: 'User not found' })
)
})
]
Set up MSW in your Jest setup:
// jest.setup.js
import '@testing-library/jest-dom'
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'
// Set up a request interception server with the given handlers
const server = setupServer(...handlers)
// Start the request interception before all tests
beforeAll(() => server.listen())
// Reset handlers between tests
afterEach(() => server.resetHandlers())
// Clean up after all tests are done
afterAll(() => server.close())
Now your tests can interact with the MSW server:
// components/UserProfile.test.js with MSW
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
import UserProfile from './UserProfile'
describe('UserProfile with MSW', () => {
test('renders user data when API call is successful', async () => {
render(<UserProfile userId="123" />)
// Check if loading state is shown
expect(screen.getByText('Loading user data...')).toBeInTheDocument()
// Wait for the loading state to be removed
await waitForElementToBeRemoved(() => screen.queryByText('Loading user data...'))
// Check if user data is rendered
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Email: [email protected]')).toBeInTheDocument()
})
})
3. Mocking Next.js Router
Next.js router is a common dependency that often needs to be mocked:
// components/NavigationButton.js
import { useRouter } from 'next/router'
export default function NavigationButton({ path, children }) {
const router = useRouter()
const handleClick = () => {
router.push(path)
}
return (
<button onClick={handleClick}>
{children}
</button>
)
}
To test this component, you need to mock the Next.js router:
// components/NavigationButton.test.js
import { render, screen, fireEvent } from '@testing-library/react'
import NavigationButton from './NavigationButton'
import { useRouter } from 'next/router'
// Mock the next/router module
jest.mock('next/router', () => ({
useRouter: jest.fn()
}))
describe('NavigationButton', () => {
test('navigates to the correct path when clicked', () => {
// Set up the mock implementation for useRouter
const pushMock = jest.fn()
useRouter.mockReturnValue({
push: pushMock
})
render(<NavigationButton path="/dashboard">Go to Dashboard</NavigationButton>)
// Find and click the button
fireEvent.click(screen.getByText('Go to Dashboard'))
// Verify that router.push was called with the correct path
expect(pushMock).toHaveBeenCalledWith('/dashboard')
})
})
4. Mocking Next.js API Routes
Testing API routes in Next.js requires mocking the request and response objects:
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}
Testing this API route:
// pages/api/hello.test.js
import { createMocks } from 'node-mocks-http'
import handler from './hello'
describe('Hello API', () => {
test('returns the correct user data', async () => {
const { req, res } = createMocks({
method: 'GET'
})
await handler(req, res)
expect(res._getStatusCode()).toBe(200)
expect(JSON.parse(res._getData())).toEqual({ name: 'John Doe' })
})
})
You'll need to install node-mocks-http
:
npm install --save-dev node-mocks-http
5. Mocking Environment Variables
Environment variables are often used for configuration in Next.js apps. To mock them in tests:
// utils/config.js
export function getApiUrl() {
return process.env.NEXT_PUBLIC_API_URL || 'https://api.default.com'
}
Testing with mocked environment variables:
// utils/config.test.js
import { getApiUrl } from './config'
describe('Config utilities', () => {
const originalEnv = process.env
beforeEach(() => {
// Make a copy of the original process.env
jest.resetModules()
process.env = { ...originalEnv }
})
afterAll(() => {
// Restore original process.env
process.env = originalEnv
})
test('returns default API URL when env var is not set', () => {
delete process.env.NEXT_PUBLIC_API_URL
expect(getApiUrl()).toBe('https://api.default.com')
})
test('returns environment variable value when set', () => {
process.env.NEXT_PUBLIC_API_URL = 'https://api.test.com'
expect(getApiUrl()).toBe('https://api.test.com')
})
})
Real-World Example: Testing a Data Dashboard
Let's create a more comprehensive example. Imagine we're building a dashboard that displays user statistics from an API:
// components/Dashboard.js
import { useEffect, useState } from 'react'
import { fetchUserStats } from '../services/statsService'
export default function Dashboard() {
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function loadStats() {
try {
setLoading(true)
const data = await fetchUserStats()
setStats(data)
} catch (err) {
setError('Failed to load statistics')
console.error(err)
} finally {
setLoading(false)
}
}
loadStats()
}, [])
if (loading) return <div data-testid="loading">Loading statistics...</div>
if (error) return <div data-testid="error">{error}</div>
return (
<div className="dashboard">
<h1>User Statistics</h1>
<div className="stats-card">
<div className="stat">
<h2>Total Users</h2>
<p data-testid="total-users">{stats.totalUsers}</p>
</div>
<div className="stat">
<h2>Active Users</h2>
<p data-testid="active-users">{stats.activeUsers}</p>
</div>
<div className="stat">
<h2>Average Engagement</h2>
<p data-testid="avg-engagement">{stats.avgEngagement}%</p>
</div>
</div>
</div>
)
}
The service that fetches the data:
// services/statsService.js
export async function fetchUserStats() {
const response = await fetch('/api/statistics')
if (!response.ok) {
throw new Error('Failed to fetch statistics')
}
return response.json()
}
Now let's test this component by mocking the statsService
:
// components/Dashboard.test.js
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
import Dashboard from './Dashboard'
import { fetchUserStats } from '../services/statsService'
// Mock the entire statistics service module
jest.mock('../services/statsService')
describe('Dashboard', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('displays loading state initially', () => {
// Mock a promise that doesn't resolve immediately
fetchUserStats.mockReturnValue(new Promise(() => {}))
render(<Dashboard />)
expect(screen.getByTestId('loading')).toBeInTheDocument()
})
test('displays statistics when data loads successfully', async () => {
// Set up the mock to return successful stats data
fetchUserStats.mockResolvedValue({
totalUsers: 12500,
activeUsers: 8750,
avgEngagement: 67.5
})
render(<Dashboard />)
// Wait for loading to finish
await waitForElementToBeRemoved(() => screen.queryByTestId('loading'))
// Check if stats are displayed
expect(screen.getByTestId('total-users')).toHaveTextContent('12500')
expect(screen.getByTestId('active-users')).toHaveTextContent('8750')
expect(screen.getByTestId('avg-engagement')).toHaveTextContent('67.5%')
})
test('displays error message when data loading fails', async () => {
// Set up the mock to simulate an error
fetchUserStats.mockRejectedValue(new Error('API error'))
render(<Dashboard />)
// Wait for loading to finish and error to appear
await waitForElementToBeRemoved(() => screen.queryByTestId('loading'))
// Check if error message is displayed
expect(screen.getByTestId('error')).toBeInTheDocument()
expect(screen.getByTestId('error')).toHaveTextContent('Failed to load statistics')
})
})
Best Practices for Mocking in Next.js
-
Mock at the right level: Mock at the boundary of your system, not internal implementation details.
-
Keep mocks simple: Only mock what is necessary for your test case.
-
Use dedicated mocking libraries: Tools like
jest-fetch-mock
or MSW make API mocking more manageable. -
Create reusable mocks: For complex objects used across multiple tests, create dedicated mock factories.
-
Restore mocks after tests: Always clean up to avoid affecting other tests.
-
Test the real thing when possible: Only mock external dependencies that would make tests slow or flaky.
-
Verify mock interactions: Assert that your mocks were called with the expected parameters.
Common Mocking Pitfalls
- Over-mocking: Mocking too many things can lead to tests that pass but don't reflect real-world behavior.
- Outdated mocks: When APIs change, mocks need to be updated too.
- Mock implementation differences: Mocks that behave differently than the real implementation can lead to false confidence.
- Not verifying mock calls: Failing to check if mocks were called correctly can lead to false positives.
Summary
Mocking is an essential technique for testing Next.js applications effectively. By replacing real services, APIs, and dependencies with controlled mock implementations, you can make your tests faster, more reliable, and more focused.
In this guide, we covered:
- Why mocking is important in Next.js testing
- Different mocking techniques for various dependencies (fetch, Next.js router, API routes)
- Using mocking libraries like MSW for more advanced mocking
- Best practices and common pitfalls
By mastering these mocking techniques, you'll be able to write comprehensive tests for your Next.js applications that run quickly and reliably, helping you catch issues earlier in development.
Further Resources
- Jest Mock Functions Documentation
- Mock Service Worker Documentation
- Testing Library Documentation
- Next.js Testing Documentation
Exercises
- Create a mock for a component that uses
getStaticProps
to fetch data at build time. - Write tests for a sign-in form that makes API calls to an authentication service.
- Test a component that uses the Next.js router for navigation between pages.
- Create a mock for a third-party service like Stripe or Firebase.
- Implement MSW to mock a REST API with multiple endpoints used by your application.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)