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:
1yarn 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:
1import type { Config } from 'jest'2import nextJest from 'next/jest'3const createJestConfig = nextJest({4 dir: './',5})6const config: Config = {7 moduleNameMapper: {8 '^@/(.)$': '<rootDir>/src/$1',9 '^next/navigation$': '<rootDir>/src/mocks/next/navigation.ts',10 },11 setupFiles: ['<rootDir>/jest.environment.ts'],12 setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],13 testEnvironment: 'jest-environment-jsdom',14 testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],15 transform: {16 '^.+\\.(ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],17 },18 transformIgnorePatterns: ['/node_modules/(?!(@mdx-js)/)'],19}20export 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:
1import { render, screen } from '@testing-library/react'2import { ProjectsContent } from '../ProjectsContent'3import { ThemeProvider } from 'styled-components'4import { theme } from '@/styles/theme'5import { Post } from '@/lib/mdx.types'67// Mock data that represents what your component expects8const mockPosts = [9 {10 content: 'Test content 1',11 cover_image: '/test-cover-image-1.jpg',12 date: '2024-01-01',13 description: 'Test description 1',14 slug: 'test-project-1',15 title: 'Test Project 1',16 },17 {18 content: 'Test content 2',19 cover_image: '/test-cover-image-2.jpg',20 date: '2024-01-02',21 description: 'Test description 2',22 slug: 'test-project-2',23 title: 'Test Project 2',24 },25] as Post[]2627// Helper function to wrap components that need theme context28const renderWithTheme = (component: React.ReactNode) => {29 return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>)30}3132describe('ProjectsContent Component', () => {33 it('renders title and project cards', () => {34 renderWithTheme(35 <ProjectsContent currentPage={1} posts={mockPosts} totalPages={1} />36 )3738 expect(screen.getByText('Projects')).toBeInTheDocument()39 expect(screen.getByText('Test Project 1')).toBeInTheDocument()40 expect(screen.getByText('Test Project 2')).toBeInTheDocument()41 })4243 it('renders pagination with correct props', () => {44 renderWithTheme(45 <ProjectsContent currentPage={2} posts={mockPosts} totalPages={3} />46 )4748 expect(screen.getByRole('navigation')).toBeInTheDocument()49 expect(screen.getByText('2')).toBeInTheDocument()50 expect(screen.getByText('3')).toBeInTheDocument()51 })52})
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:
1import { fireEvent, render, screen, waitFor } from '@testing-library/react'2import { ContactForm } from '../ContactForm'3import { ThemeProvider } from 'styled-components'4import { theme } from '@/styles/theme'56const renderWithTheme = (component: React.ReactNode) => {7 return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>)8}910describe('ContactForm Component', () => {11 beforeEach(() => {12 // Reset fetch mock before each test13 global.fetch = jest.fn()14 })1516 it('validates required fields before submission', () => {17 renderWithTheme(<ContactForm />)1819 // Try to submit empty form20 const submitButton = screen.getByRole('button', { name: /send message/i })21 fireEvent.click(submitButton)2223 // Check for required field validation24 expect(submitButton).toBeDisabled()25 })2627 it('handles form submission successfully', async () => {28 // Mock successful fetch response29 global.fetch = jest.fn().mockResolvedValueOnce({})3031 renderWithTheme(<ContactForm />)3233 // Fill out form34 fireEvent.change(screen.getByLabelText(/name/i), {35 target: { value: 'Test User' },36 })37 fireEvent.change(screen.getByLabelText(/email/i), {38 target: { value: 'test@example.com' },39 })40 fireEvent.change(screen.getByLabelText(/subject/i), {41 target: { value: 'Test Subject' },42 })43 fireEvent.change(screen.getByLabelText(/message/i), {44 target: { value: 'Test message content' },45 })4647 // Submit form48 fireEvent.click(screen.getByRole('button', { name: /send message/i }))4950 // Wait for success message51 await waitFor(() => {52 expect(screen.getByText(/message sent successfully/i)).toBeInTheDocument()53 })5455 // Verify form was reset56 expect(screen.getByLabelText(/name/i)).toHaveValue('')57 expect(screen.getByLabelText(/message/i)).toHaveValue('')58 })5960 it('handles submission errors', async () => {61 // Mock failed fetch response62 global.fetch = jest.fn().mockRejectedValueOnce(new Error('Failed to send'))6364 renderWithTheme(<ContactForm />)6566 // Fill and submit form67 fireEvent.change(screen.getByLabelText(/name/i), {68 target: { value: 'Test User' },69 })70 fireEvent.change(screen.getByLabelText(/email/i), {71 target: { value: 'test@example.com' },72 })73 fireEvent.change(screen.getByLabelText(/message/i), {74 target: { value: 'Test message' },75 })7677 fireEvent.click(screen.getByRole('button', { name: /send message/i }))7879 // Check for error message80 await waitFor(() => {81 expect(screen.getByText(/failed to send message/i)).toBeInTheDocument()82 })83 })84})
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
:
1"scripts": {2 "test": "jest",3 "test:watch": "jest --watch"4}
Then, run your tests with:
1yarn 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!