
Testing Your Next.js Site
Introduction
Testing is a crucial part of modern web development, ensuring that your application behaves as expected. In this post, we'll explore how to set up and use Jest with a Next.js application.
Setting Up Jest
To get started with Jest in your Next.js project, you'll need to install several dependencies. You can do this using Yarn:
yarn add --dev jest jest-environment-jsdom @testing-library/react @testing-library/react-hooks @testing-library/dom @testing-library/jest-dom ts-node @types/jest @types/react @types/react-domThese packages will provide the necessary tools for unit testing and snapshot testing in your Next.js application.
Configuring Jest
Next, you'll need to configure Jest to work with Next.js. Create a jest.config.ts file in the root of your project:
import type { Config } from 'jest'
import nextJest from 'next/jest'
const createJestConfig = nextJest({
dir: './',
})
const config: Config = {
moduleNameMapper: {
'^@/(.)$': '<rootDir>/src/$1',
'^next/navigation$': '<rootDir>/src/mocks/next/navigation.ts',
},
setupFiles: ['<rootDir>/jest.environment.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jest-environment-jsdom',
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
transform: {
'^.+\\.(ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
},
transformIgnorePatterns: ['/node_modules/(?!(@mdx-js)/)'],
}
export default createJestConfig(config)This configuration sets up Jest to handle TypeScript and module aliases, and it uses jest-environment-jsdom for testing React components.
Writing Tests
Create a __tests__ directory in your project to store your test files. Let's look at a real-world example testing a component that displays a list of projects:
import { render, screen } from '@testing-library/react'
import { ProjectsContent } from '../ProjectsContent'
import { ThemeProvider } from 'styled-components'
import { theme } from '@/styles/theme'
import { Post } from '@/lib/mdx.types'
// Mock data that represents what your component expects
const mockPosts = [
{
content: 'Test content 1',
cover_image: '/test-cover-image-1.jpg',
date: '2024-01-01',
description: 'Test description 1',
slug: 'test-project-1',
title: 'Test Project 1',
},
{
content: 'Test content 2',
cover_image: '/test-cover-image-2.jpg',
date: '2024-01-02',
description: 'Test description 2',
slug: 'test-project-2',
title: 'Test Project 2',
},
] as Post[]
// Helper function to wrap components that need theme context
const renderWithTheme = (component: React.ReactNode) => {
return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>)
}
describe('ProjectsContent Component', () => {
it('renders title and project cards', () => {
renderWithTheme(
<ProjectsContent currentPage={1} posts={mockPosts} totalPages={1} />
)
expect(screen.getByText('Projects')).toBeInTheDocument()
expect(screen.getByText('Test Project 1')).toBeInTheDocument()
expect(screen.getByText('Test Project 2')).toBeInTheDocument()
})
it('renders pagination with correct props', () => {
renderWithTheme(
<ProjectsContent currentPage={2} posts={mockPosts} totalPages={3} />
)
expect(screen.getByRole('navigation')).toBeInTheDocument()
expect(screen.getByText('2')).toBeInTheDocument()
expect(screen.getByText('3')).toBeInTheDocument()
})
})This example demonstrates several important testing concepts:
- Mocking Data: Creating realistic test data that matches your types
- Context Providers: Wrapping components that need context (like styled-components' theme)
- Multiple Test Cases: Testing different scenarios in separate test blocks
- Accessibility Testing: Using roles to find elements (
getByRole) - Component Integration: Testing how multiple components work together (ProjectsContent with Pagination)
This test uses @testing-library/react to render the component and check if the text "learn react" is present in the document.
Let's look at another example that tests a contact form component with user interactions and form submission:
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { ContactForm } from '../ContactForm'
import { ThemeProvider } from 'styled-components'
import { theme } from '@/styles/theme'
const renderWithTheme = (component: React.ReactNode) => {
return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>)
}
describe('ContactForm Component', () => {
beforeEach(() => {
// Reset fetch mock before each test
global.fetch = jest.fn()
})
it('validates required fields before submission', () => {
renderWithTheme(<ContactForm />)
// Try to submit empty form
const submitButton = screen.getByRole('button', { name: /send message/i })
fireEvent.click(submitButton)
// Check for required field validation
expect(submitButton).toBeDisabled()
})
it('handles form submission successfully', async () => {
// Mock successful fetch response
global.fetch = jest.fn().mockResolvedValueOnce({})
renderWithTheme(<ContactForm />)
// Fill out form
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'Test User' },
})
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' },
})
fireEvent.change(screen.getByLabelText(/subject/i), {
target: { value: 'Test Subject' },
})
fireEvent.change(screen.getByLabelText(/message/i), {
target: { value: 'Test message content' },
})
// Submit form
fireEvent.click(screen.getByRole('button', { name: /send message/i }))
// Wait for success message
await waitFor(() => {
expect(screen.getByText(/message sent successfully/i)).toBeInTheDocument()
})
// Verify form was reset
expect(screen.getByLabelText(/name/i)).toHaveValue('')
expect(screen.getByLabelText(/message/i)).toHaveValue('')
})
it('handles submission errors', async () => {
// Mock failed fetch response
global.fetch = jest.fn().mockRejectedValueOnce(new Error('Failed to send'))
renderWithTheme(<ContactForm />)
// Fill and submit form
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'Test User' },
})
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' },
})
fireEvent.change(screen.getByLabelText(/message/i), {
target: { value: 'Test message' },
})
fireEvent.click(screen.getByRole('button', { name: /send message/i }))
// Check for error message
await waitFor(() => {
expect(screen.getByText(/failed to send message/i)).toBeInTheDocument()
})
})
})This example showcases:
- User Interaction Testing: Using
fireEventto simulate user input - Async Testing: Using
waitForto handle async operations - Mocking External Calls: Mocking the
fetchAPI - Form Validation: Testing required fields and validation states
- Error Handling: Testing both success and error scenarios
- State Changes: Verifying form reset after submission
Running Tests
To run your tests, add a script to your package.json:
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}Then, run your tests with:
yarn testThis will run all your tests and provide a summary of the results.
Conclusion
By following these steps, you'll have a solid foundation for testing your Next.js application with Jest. This setup allows you to write tests for your components, hooks, and other parts of your application.
Happy testing!