πŸš€ Comprehensive Guide to Testing Next.js Applications with Jest

This documentation provides a comprehensive guide for implementing and maintaining a robust testing environment in Next.js applications using Jest. The guide covers setup, configuration, and best practices for writing effective tests.

Installation

Step 1: Install Required Dependencies

# Core testing dependencies
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

# TypeScript support
npm install --save-dev ts-jest @types/jest @types/testing-library__jest-dom

Step 2: Verify Installation

Confirm the installation by checking your package.json for the following devDependencies:

{
  "devDependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "jest": "^29.3.1",
    "jest-environment-jsdom": "^29.3.1",
    "ts-jest": "^29.0.3"
  }
}

Configuration

Step 1: Create Jest Configuration File

Create jest.config.js in your project root:

const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: './',
})

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
  },
}

module.exports = createJestConfig(customJestConfig)

Step 2: Create Jest Setup File

Create jest.setup.js in your project root:

import '@testing-library/jest-dom'

// Mock next/router
jest.mock('next/router', () => ({
  useRouter() {
    return {
      route: '/',
      pathname: '',
      query: '',
      asPath: '',
      push: jest.fn(),
      replace: jest.fn(),
    }
  },
}))

// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
})

Step 3: Update TypeScript Configuration

Update tsconfig.json:

{
  "compilerOptions": {
    "types": ["jest", "node", "@testing-library/jest-dom"]
  }
}

Test Structure

Recommended Directory Structure

src/
  β”œβ”€β”€ components/
  β”‚   β”œβ”€β”€ __tests__/
  β”‚   β”‚   └── ComponentName.test.tsx
  β”‚   └── ComponentName.tsx
  β”œβ”€β”€ hooks/
  β”‚   β”œβ”€β”€ __tests__/
  β”‚   β”‚   └── useHookName.test.ts
  β”‚   └── useHookName.ts
  └── app/
      β”œβ”€β”€ __tests__/
      β”‚   └── service-worker-registration.test.ts
      └── service-worker-registration.ts

Writing Tests

Basic Test Structure

import { render, screen } from '@testing-library/react'
import ComponentName from '../ComponentName'

describe('ComponentName', () => {
  beforeEach(() => {
    // Setup code
  })

  afterEach(() => {
    // Cleanup code
  })

  it('should render correctly', () => {
    render(<ComponentName />)
    expect(screen.getByText('Expected Text')).toBeInTheDocument()
  })
})

Component Testing

import { render, screen, fireEvent } from '@testing-library/react'
import Button from '../Button'

describe('Button', () => {
  it('should call onClick handler when clicked', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

Hook Testing

import { renderHook, act } from '@testing-library/react'
import useCounter from '../useCounter'

describe('useCounter', () => {
  it('should increment counter', () => {
    const { result } = renderHook(() => useCounter())
    
    act(() => {
      result.current.increment()
    })
    
    expect(result.current.count).toBe(1)
  })
})

Testing Patterns

Mocking Dependencies

// Mock a module
jest.mock('../api', () => ({
  fetchData: jest.fn().mockResolvedValue({ data: 'test' })
}))

// Mock a hook
jest.mock('../hooks/useAuth', () => ({
  useAuth: () => ({
    user: { id: 1, name: 'Test User' },
    isAuthenticated: true
  })
}))

Testing Async Operations

it('should handle async data fetching', async () => {
  const { result } = renderHook(() => useData())
  
  await act(async () => {
    await result.current.fetchData()
  })
  
  expect(result.current.data).toEqual({ id: 1, name: 'Test' })
})

Testing Error States

it('should handle errors', async () => {
  jest.spyOn(console, 'error').mockImplementation(() => {})
  
  const { result } = renderHook(() => useData())
  
  await act(async () => {
    await result.current.fetchData()
  })
  
  expect(result.current.error).toBeTruthy()
  expect(console.error).toHaveBeenCalled()
})

:bar_chart: Setting Up Code Coverage

  1. Installation:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
  1. Configuration (jest.config.js):
module.exports = {
  testEnvironment: 'jsdom',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}'
  ]
}
  1. Test Scripts (package.json):
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}
  1. Running Tests:
# Run all tests
npm test

# Run tests with coverage report
npm run test:coverage

# Run tests in watch mode
npm run test:watch
  1. Coverage Report:
  • HTML report: coverage/lcov-report/index.html
  • Console summary after running tests
  • Detailed line-by-line coverage in HTML report
  1. Current Coverage Status:
  • Function Coverage: 100%
  • Line Coverage: 100%
  • Branch Coverage: 100%

Best Practices

  1. Test Organization:
  • Group related tests using describe
  • Use clear, descriptive test names
  • Follow the Arrange-Act-Assert pattern
  1. Test Isolation:
  • Each test should be independent
  • Clean up after each test
  • Reset mocks between tests
  1. Test Coverage:
  • Aim for meaningful coverage
  • Focus on critical paths
  • Test edge cases and error scenarios
  1. Performance:
  • Keep tests fast and focused
  • Avoid unnecessary setup
  • Use appropriate mocking strategies

Additional Resources

Conclusion

This guide provides a comprehensive foundation for testing Next.js applications. By following these practices, you can build a robust test suite that helps maintain code quality and prevent regressions. Remember that testing is an ongoing process, and these practices should evolve with your project’s needs.

For further assistance or specific questions, please refer to the official documentation or community resources mentioned above.

2 Likes

You can also post an internal document sheet if it covers any other use cases along with additional resources @pavitra.kini

Sure @aishwarya I will do this.