Next.js React Testing Library
React Testing Library has become the go-to choice for testing React components, emphasizing testing components as users would interact with them. In this guide, we'll explore how to use React Testing Library specifically with Next.js applications to write effective, maintainable tests.
Introduction
React Testing Library encourages testing your components based on how users interact with your application rather than focusing on implementation details. This approach ensures your tests remain useful even as your implementation evolves. When combining React Testing Library with Next.js, there are a few specific considerations and configurations to keep in mind.
Setting Up Testing Environment
Before we start writing tests, we need to set up our testing environment for a Next.js project.
Installation
First, let's install the required dependencies:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest jest-environment-jsdom
Configuration
Create a Jest configuration file jest.config.js
in your project root:
const nextJest = require('next/jest')
const createJestConfig = nextJest({
// Provide the path to your Next.js app
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 file jest.setup.js
:
// Optional: configure or set up a testing framework before each test.
import '@testing-library/jest-dom/extend-expect'
Update your package.json
to include test scripts:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
}
Basic Component Testing
Let's start by testing a simple Next.js component.
Example: Testing a Button Component
Assume we have a simple button component:
// components/Button.jsx
export default function Button({ onClick, children }) {
return (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={onClick}
>
{children}
</button>
);
}
Here's how we would test this component:
// __tests__/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import Button from '../components/Button'
describe('Button', () => {
it('renders a button with the provided text', () => {
render(<Button>Click Me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('calls the onClick handler when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click Me</Button>)
fireEvent.click(screen.getByRole('button', { name: /click me/i }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
Testing Pages with Data Fetching
Next.js pages often fetch data. Let's see how to test a page component that uses getServerSideProps
or getStaticProps
.
Example: Testing a Page with Data
Assume we have this page component:
// pages/users.jsx
export default function Users({ users }) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
export async function getStaticProps() {
// In a real app, you might fetch this from an API
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
return {
props: {
users,
},
}
}
Here's how we can test this page component:
// __tests__/users.test.jsx
import { render, screen } from '@testing-library/react'
import Users from '../pages/users'
describe('Users Page', () => {
it('renders a list of users', () => {
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
render(<Users users={mockUsers} />)
expect(screen.getByRole('heading', { name: /users/i })).toBeInTheDocument()
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
})
})
Testing Navigation and Routing
Next.js uses its own routing system, which requires special handling in tests.
Setting Up Test Router
Create a helper function to wrap components with the Next.js router context:
// test-utils/router.jsx
import { RouterContext } from 'next/dist/shared/lib/router-context'
export function createMockRouter(router) {
return {
basePath: '',
pathname: '/',
route: '/',
asPath: '/',
query: {},
push: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
back: jest.fn(),
prefetch: jest.fn(),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
...router,
}
}
Example: Testing a Navigation Component
// components/NavLink.jsx
import Link from 'next/link'
import { useRouter } from 'next/router'
export default function NavLink({ href, children }) {
const router = useRouter()
const isActive = router.pathname === href
return (
<Link href={href}>
<a className={isActive ? 'active' : ''}>{children}</a>
</Link>
)
}
Test for the NavLink component:
// __tests__/NavLink.test.jsx
import { render, screen } from '@testing-library/react'
import { RouterContext } from 'next/dist/shared/lib/router-context'
import { createMockRouter } from '../test-utils/router'
import NavLink from '../components/NavLink'
describe('NavLink', () => {
it('renders the link with the active class when pathname matches href', () => {
const router = createMockRouter({ pathname: '/about' })
render(
<RouterContext.Provider value={router}>
<NavLink href="/about">About</NavLink>
</RouterContext.Provider>
)
const link = screen.getByText('About')
expect(link).toHaveClass('active')
})
it('renders the link without the active class when pathname does not match href', () => {
const router = createMockRouter({ pathname: '/home' })
render(
<RouterContext.Provider value={router}>
<NavLink href="/about">About</NavLink>
</RouterContext.Provider>
)
const link = screen.getByText('About')
expect(link).not.toHaveClass('active')
})
})
Testing API Routes
Next.js API routes can be tested using a combination of Jest and a lightweight HTTP client.
Example: Testing an API Route
First, let's create a simple API route:
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}
Now let's test it:
// __tests__/api/hello.test.js
import { createMocks } from 'node-mocks-http'
import handler from '../../pages/api/hello'
describe('/api/hello', () => {
it('returns a successful response with JSON', 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' })
})
})
Note: You'll need to install the node-mocks-http
package:
npm install --save-dev node-mocks-http
Testing with User Interactions
React Testing Library provides @testing-library/user-event
which simulates user interactions more realistically than fireEvent
.
Example: Testing a Form Component
Assume we have a simple form component:
// components/LoginForm.jsx
import { useState } from 'react'
export default function LoginForm({ onSubmit }) {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
onSubmit({ username, password })
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Login</button>
</form>
)
}
Here's how we would test this form using userEvent
:
// __tests__/LoginForm.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import LoginForm from '../components/LoginForm'
describe('LoginForm', () => {
it('submits the form with user input', async () => {
const handleSubmit = jest.fn()
const user = userEvent.setup()
render(<LoginForm onSubmit={handleSubmit} />)
await user.type(screen.getByLabelText(/username/i), 'testuser')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /login/i }))
expect(handleSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123'
})
})
})
Testing with Mocked API Calls
When components make API calls, we usually want to mock these calls in our tests.
Example: Testing a Component with useEffect Data Fetching
// components/UserList.jsx
import { useState, useEffect } from 'react'
export default function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchUsers = async () => {
try {
const res = await fetch('/api/users')
if (!res.ok) throw new Error('Failed to fetch')
const data = await res.json()
setUsers(data)
setLoading(false)
} catch (error) {
setError('Failed to load users')
setLoading(false)
}
}
fetchUsers()
}, [])
if (loading) return <p>Loading...</p>
if (error) return <p>{error}</p>
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Here's how we can test this component by mocking the fetch API:
// __tests__/UserList.test.jsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
import UserList from '../components/UserList'
// Mock the global fetch function
global.fetch = jest.fn()
describe('UserList', () => {
it('displays a list of users when fetch is successful', async () => {
// Mock successful response
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
],
})
render(<UserList />)
// Initially shows loading state
expect(screen.getByText('Loading...')).toBeInTheDocument()
// Wait for loading to disappear
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'))
// Check if users are displayed
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
})
it('displays an error when fetch fails', async () => {
// Mock failed response
global.fetch.mockResolvedValueOnce({
ok: false,
})
render(<UserList />)
// Initially shows loading state
expect(screen.getByText('Loading...')).toBeInTheDocument()
// Wait for loading to disappear
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'))
// Check if error message is displayed
expect(screen.getByText('Failed to load users')).toBeInTheDocument()
})
})
Testing with Custom Hooks
If your Next.js application uses custom hooks, they should be tested within components rather than in isolation.
Example: Testing a Component Using a Custom Hook
// hooks/useCounter.js
import { useState } from 'react'
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = () => setCount(prev => prev + 1)
const decrement = () => setCount(prev => prev - 1)
return { count, increment, decrement }
}
// components/Counter.jsx
import { useCounter } from '../hooks/useCounter'
export default function Counter() {
const { count, increment, decrement } = useCounter()
return (
<div>
<p data-testid="count">{count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
)
}
Testing the Counter component:
// __tests__/Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import Counter from '../components/Counter'
describe('Counter', () => {
it('increments the count when increment button is clicked', () => {
render(<Counter />)
// Check initial count
expect(screen.getByTestId('count')).toHaveTextContent('0')
// Click increment button
fireEvent.click(screen.getByRole('button', { name: /increment/i }))
// Check updated count
expect(screen.getByTestId('count')).toHaveTextContent('1')
})
it('decrements the count when decrement button is clicked', () => {
render(<Counter />)
// Check initial count
expect(screen.getByTestId('count')).toHaveTextContent('0')
// Click decrement button
fireEvent.click(screen.getByRole('button', { name: /decrement/i }))
// Check updated count
expect(screen.getByTestId('count')).toHaveTextContent('-1')
})
})
Best Practices for Next.js Testing
-
Test User Interactions: Focus on testing how users interact with your application rather than implementation details.
-
Mock Data and API Calls: Use mocks for data fetching to keep tests fast and reliable.
-
Use Test IDs Sparingly: Prefer testing user-facing attributes like text content, ARIA roles, and labels over test IDs.
-
Set Up Global Test Utils: Create helper functions to handle common testing scenarios like setting up the router or providing contexts.
-
Test Rendering Modes: Test both client-side and server-side rendering aspects of your Next.js components when relevant.
-
Isolate Tests: Keep tests isolated so they don't influence each other.
-
Test Mobile and Desktop Views: Test responsive behaviors if your components behave differently on different screen sizes.
-
Use Testing Library's findBy Methods for Async Operations: These methods wait for elements to appear in the DOM.
Summary
Testing Next.js applications with React Testing Library provides a robust way to ensure your components work as expected. By focusing on user interactions rather than implementation details, your tests will remain valuable even as your code evolves. The combination of React Testing Library's user-centric approach with Next.js's unique features like server-side rendering and routing creates a powerful testing environment.
Remember to:
- Set up your testing environment correctly
- Mock external dependencies appropriately
- Test components as a user would interact with them
- Consider Next.js-specific features like routing and data fetching
Additional Resources
- React Testing Library Documentation
- Next.js Testing Documentation
- Jest Documentation
- Testing Library Cheatsheet
Exercises
-
Create a simple Next.js component with a button that toggles a text's visibility, then write tests to verify the toggle functionality.
-
Write tests for a form component that validates user input and displays error messages.
-
Create a test for a Next.js page that uses
getServerSideProps
to fetch data. -
Write tests for a navigation component that highlights the active route in your Next.js application.
-
Create and test a component that fetches data from an API in a useEffect hook, handling loading and error states.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)