Testing Your Next.js Site

Testing Your Next.js Site

September 01, 20245 min read

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'
6
7// Mock data that represents what your component expects
8const 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[]
26
27// Helper function to wrap components that need theme context
28const renderWithTheme = (component: React.ReactNode) => {
29 return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>)
30}
31
32describe('ProjectsContent Component', () => {
33 it('renders title and project cards', () => {
34 renderWithTheme(
35 <ProjectsContent currentPage={1} posts={mockPosts} totalPages={1} />
36 )
37
38 expect(screen.getByText('Projects')).toBeInTheDocument()
39 expect(screen.getByText('Test Project 1')).toBeInTheDocument()
40 expect(screen.getByText('Test Project 2')).toBeInTheDocument()
41 })
42
43 it('renders pagination with correct props', () => {
44 renderWithTheme(
45 <ProjectsContent currentPage={2} posts={mockPosts} totalPages={3} />
46 )
47
48 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:

  1. Mocking Data: Creating realistic test data that matches your types
  2. Context Providers: Wrapping components that need context (like styled-components' theme)
  3. Multiple Test Cases: Testing different scenarios in separate test blocks
  4. Accessibility Testing: Using roles to find elements (getByRole)
  5. 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'
5
6const renderWithTheme = (component: React.ReactNode) => {
7 return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>)
8}
9
10describe('ContactForm Component', () => {
11 beforeEach(() => {
12 // Reset fetch mock before each test
13 global.fetch = jest.fn()
14 })
15
16 it('validates required fields before submission', () => {
17 renderWithTheme(<ContactForm />)
18
19 // Try to submit empty form
20 const submitButton = screen.getByRole('button', { name: /send message/i })
21 fireEvent.click(submitButton)
22
23 // Check for required field validation
24 expect(submitButton).toBeDisabled()
25 })
26
27 it('handles form submission successfully', async () => {
28 // Mock successful fetch response
29 global.fetch = jest.fn().mockResolvedValueOnce({})
30
31 renderWithTheme(<ContactForm />)
32
33 // Fill out form
34 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 })
46
47 // Submit form
48 fireEvent.click(screen.getByRole('button', { name: /send message/i }))
49
50 // Wait for success message
51 await waitFor(() => {
52 expect(screen.getByText(/message sent successfully/i)).toBeInTheDocument()
53 })
54
55 // Verify form was reset
56 expect(screen.getByLabelText(/name/i)).toHaveValue('')
57 expect(screen.getByLabelText(/message/i)).toHaveValue('')
58 })
59
60 it('handles submission errors', async () => {
61 // Mock failed fetch response
62 global.fetch = jest.fn().mockRejectedValueOnce(new Error('Failed to send'))
63
64 renderWithTheme(<ContactForm />)
65
66 // Fill and submit form
67 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 })
76
77 fireEvent.click(screen.getByRole('button', { name: /send message/i }))
78
79 // Check for error message
80 await waitFor(() => {
81 expect(screen.getByText(/failed to send message/i)).toBeInTheDocument()
82 })
83 })
84})

This example showcases:

  1. User Interaction Testing: Using fireEvent to simulate user input
  2. Async Testing: Using waitFor to handle async operations
  3. Mocking External Calls: Mocking the fetch API
  4. Form Validation: Testing required fields and validation states
  5. Error Handling: Testing both success and error scenarios
  6. 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!