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:

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:

  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:

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:

  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:

"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!