Testing Your Next.js Site
title: 'Testing Your Next.js Site' description: 'How to test your Next.js site' date: '2024-09-01' section: blog cover_image: 'https://res.cloudinary.com/crbaucom/image/upload/v1734028955/crbaucom-images/testing-nextjs.png' tags: ['nextjs', 'testing', 'typescript', 'react']
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-dom
These 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
fireEvent
to simulate user input - Async Testing: Using
waitFor
to handle async operations - Mocking External Calls: Mocking the
fetch
API - 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 test
This 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!