Next.js Component Testing
Introduction
Component testing is a critical part of building reliable Next.js applications. It allows you to verify that your UI components render correctly and behave as expected when users interact with them. In this guide, we'll explore how to set up and write effective component tests for your Next.js applications.
Component tests focus on testing individual React components in isolation, ensuring they render correctly and respond appropriately to user interactions. This approach helps you catch bugs early in the development process and provides confidence when refactoring or adding new features.
Setting Up Testing Environment
Before we start writing component tests, we need to set up a testing environment. Next.js provides built-in support for testing with Jest and React Testing Library.
Installing Dependencies
First, let's install the necessary dependencies:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
Configuring Jest
Create a jest.config.js
file in the root of your project:
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)
Setting Up Jest Extensions
Create a jest.setup.js
file to import testing utilities:
// Optional: configure or set up a testing framework before each test
import '@testing-library/jest-dom/extend-expect'
Update package.json
Add test scripts to your package.json
:
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
Testing Basic Components
Let's start by testing a simple component. We'll create a button component and test its rendering and behavior.
Example 1: Testing a Button Component
First, create a Button component:
components/Button.js
export default function Button({ label, onClick }) {
return (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={onClick}
>
{label}
</button>
);
}
Now, let's write a test for this component:
components/Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button Component', () => {
test('renders button with correct label', () => {
render(<Button label="Click Me" />);
const buttonElement = screen.getByText('Click Me');
expect(buttonElement).toBeInTheDocument();
});
test('calls onClick handler when clicked', () => {
const mockOnClick = jest.fn();
render(<Button label="Click Me" onClick={mockOnClick} />);
const buttonElement = screen.getByText('Click Me');
fireEvent.click(buttonElement);
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
});
Running Tests
Run your tests with:
npm test
Output should look something like:
PASS components/Button.test.js
Button Component
✓ renders button with correct label (28 ms)
✓ calls onClick handler when clicked (3 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.752 s
Testing Complex Components
Now let's test a more complex component that includes state management and user interaction.
Example 2: Testing a Counter Component
components/Counter.js
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div className="p-4 border rounded shadow-sm">
<h2 className="text-xl font-bold mb-4">Counter: {count}</h2>
<div className="flex space-x-2">
<button
onClick={decrement}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
data-testid="decrement"
>
Decrease
</button>
<button
onClick={increment}
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
data-testid="increment"
>
Increase
</button>
<button
onClick={reset}
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded"
data-testid="reset"
>
Reset
</button>
</div>
</div>
);
}
components/Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
describe('Counter Component', () => {
test('renders counter with initial value of 0', () => {
render(<Counter />);
expect(screen.getByText(/counter: 0/i)).toBeInTheDocument();
});
test('increments counter when increase button is clicked', () => {
render(<Counter />);
const incrementButton = screen.getByTestId('increment');
fireEvent.click(incrementButton);
expect(screen.getByText(/counter: 1/i)).toBeInTheDocument();
});
test('decrements counter when decrease button is clicked', () => {
render(<Counter />);
const decrementButton = screen.getByTestId('decrement');
fireEvent.click(decrementButton);
expect(screen.getByText(/counter: -1/i)).toBeInTheDocument();
});
test('resets counter when reset button is clicked', () => {
render(<Counter />);
// First increment to change the initial state
const incrementButton = screen.getByTestId('increment');
fireEvent.click(incrementButton);
fireEvent.click(incrementButton);
expect(screen.getByText(/counter: 2/i)).toBeInTheDocument();
// Then reset
const resetButton = screen.getByTestId('reset');
fireEvent.click(resetButton);
expect(screen.getByText(/counter: 0/i)).toBeInTheDocument();
});
});
Testing Next.js Pages
Testing Next.js page components can be a bit more complex due to routing, data fetching, and other Next.js specific features.
Example 3: Testing a Simple Page Component
pages/about.js
export default function AboutPage() {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">About Us</h1>
<p>
We are a company dedicated to building amazing web experiences with Next.js.
</p>
<div className="mt-4">
<a href="/" className="text-blue-500 hover:underline">
Back to Home
</a>
</div>
</div>
);
}
pages/about.test.js
import { render, screen } from '@testing-library/react';
import AboutPage from './about';
describe('AboutPage', () => {
test('renders about page content', () => {
render(<AboutPage />);
expect(screen.getByText(/about us/i)).toBeInTheDocument();
expect(screen.getByText(/dedicated to building amazing web experiences/i)).toBeInTheDocument();
expect(screen.getByText(/back to home/i)).toBeInTheDocument();
});
});
Testing Components with Data Fetching
Components that fetch data need special care when testing. We'll mock the data fetching process to test these components effectively.
Example 4: Testing a Component with Data Fetching
components/UserProfile.js
import { useState, useEffect } from 'react';
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div data-testid="loading">Loading...</div>;
if (error) return <div data-testid="error">Error: {error}</div>;
if (!user) return null;
return (
<div className="border p-4 rounded shadow-sm">
<h2 className="text-xl font-bold mb-2" data-testid="user-name">{user.name}</h2>
<p data-testid="user-email">Email: {user.email}</p>
<p data-testid="user-phone">Phone: {user.phone}</p>
</div>
);
}
components/UserProfile.test.js
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
// Mock the global fetch function
global.fetch = jest.fn();
describe('UserProfile Component', () => {
beforeEach(() => {
fetch.mockClear();
});
test('displays loading state initially', () => {
// Mock a pending promise that doesn't resolve during the test
fetch.mockImplementationOnce(() => new Promise(() => {}));
render(<UserProfile userId={1} />);
expect(screen.getByTestId('loading')).toBeInTheDocument();
});
test('displays user data after successful fetch', async () => {
// Mock a successful API response
fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({
id: 1,
name: 'John Doe',
email: '[email protected]',
phone: '123-456-7890'
})
})
);
render(<UserProfile userId={1} />);
// Wait for the component to update with fetched data
await waitFor(() => {
expect(screen.getByTestId('user-name')).toBeInTheDocument();
});
expect(screen.getByTestId('user-name')).toHaveTextContent('John Doe');
expect(screen.getByTestId('user-email')).toHaveTextContent('[email protected]');
expect(screen.getByTestId('user-phone')).toHaveTextContent('123-456-7890');
});
test('displays error message when fetch fails', async () => {
// Mock a failed API response
fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: false
})
);
render(<UserProfile userId={1} />);
// Wait for the error message to appear
await waitFor(() => {
expect(screen.getByTestId('error')).toBeInTheDocument();
});
expect(screen.getByTestId('error')).toHaveTextContent('Failed to fetch user');
});
});
Testing Components with Context
Many Next.js applications use React Context for state management. Let's see how to test components that consume context.
Example 5: Testing a Component with Context
First, let's create a theme context:
contexts/ThemeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Now, create a component that uses this context:
components/ThemeSwitcher.js
import { useTheme } from '../contexts/ThemeContext';
export default function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
return (
<div
className={`p-4 ${theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-black'}`}
data-testid="theme-container"
>
<p>Current theme: {theme}</p>
<button
onClick={toggleTheme}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
data-testid="theme-toggle"
>
Toggle Theme
</button>
</div>
);
}
Now, let's test this component:
components/ThemeSwitcher.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import ThemeSwitcher from './ThemeSwitcher';
import { ThemeProvider } from '../contexts/ThemeContext';
// Create a wrapper component that includes the provider
const renderWithThemeProvider = (component) => {
return render(
<ThemeProvider>
{component}
</ThemeProvider>
);
};
describe('ThemeSwitcher Component', () => {
test('renders with light theme by default', () => {
renderWithThemeProvider(<ThemeSwitcher />);
expect(screen.getByText(/current theme: light/i)).toBeInTheDocument();
});
test('toggles theme when button is clicked', () => {
renderWithThemeProvider(<ThemeSwitcher />);
// Initially light theme
expect(screen.getByText(/current theme: light/i)).toBeInTheDocument();
// Click the toggle button
fireEvent.click(screen.getByTestId('theme-toggle'));
// Should now be dark theme
expect(screen.getByText(/current theme: dark/i)).toBeInTheDocument();
// Click again to toggle back
fireEvent.click(screen.getByTestId('theme-toggle'));
// Should be light theme again
expect(screen.getByText(/current theme: light/i)).toBeInTheDocument();
});
});
Best Practices for Next.js Component Testing
- Test in isolation: Focus on testing one component at a time.
- Use data-testid attributes: Avoid testing implementation details by selecting elements with data-testid.
- Mock external dependencies: APIs, context providers, and other external dependencies should be mocked.
- Test user interactions: Make sure buttons, forms, and other interactive elements work as expected.
- Test different states: Test loading states, error states, and success states.
- Test responsiveness: For UI components, consider testing behavior at different screen sizes.
- Keep tests simple and focused: Each test should verify a single behavior.
- Use RTL queries appropriately: Prefer
getByRole
andgetByText
overgetByTestId
when possible.
Common Testing Patterns
Testing Forms
test('form submission works correctly', () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
fireEvent.change(screen.getByLabelText(/username/i), {
target: { value: 'testuser' }
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'password123' }
});
fireEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(handleSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123'
});
});
Testing Asynchronous Updates
test('displays data after async update', async () => {
render(<AsyncComponent />);
// Assert loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
// Assert data is displayed
expect(screen.getByText(/data loaded/i)).toBeInTheDocument();
});
Advanced Testing Techniques
Snapshot Testing
Snapshot testing is useful for detecting unexpected UI changes:
import { render } from '@testing-library/react';
import Button from './Button';
test('button component matches snapshot', () => {
const { container } = render(<Button label="Click Me" />);
expect(container).toMatchSnapshot();
});
Testing with Custom Hooks
If your component uses custom hooks, test the hook separately:
hooks/useCounter.js
import { useState } from 'react';
export default function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialCount);
return { count, increment, decrement, reset };
}
hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
test('should reset counter', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
Summary
In this guide, we explored how to effectively test components in Next.js applications:
- We set up a testing environment with Jest and React Testing Library
- We learned how to test basic components and verify their rendering
- We explored testing user interactions with components
- We covered testing complex components with state management
- We learned how to test components that fetch data
- We demonstrated testing components that use context
- We covered best practices and common testing patterns
Component testing is essential for building reliable Next.js applications. By writing comprehensive tests, you can ensure your components behave correctly and catch bugs early. Remember to focus on testing behavior rather than implementation details, and use the testing tools effectively to create maintainable tests.
Additional Resources
- React Testing Library Documentation
- Jest Documentation
- Next.js Testing Documentation
- Testing React hooks with @testing-library/react-hooks
Exercises
- Create a simple form component with input validation and write tests to verify the validation works correctly.
- Write tests for a navigation menu component that handles responsive behavior.
- Create a component that uses the fetch API to load a list of items and write tests to verify loading, success, and error states.
- Implement a theme switcher with context and write tests to verify theme changes are applied correctly.
- Create a pagination component and write tests to verify page navigation works correctly.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)