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()
})
Setting Up Code Coverage
- Installation:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
- 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}'
]
}
- Test Scripts (package.json):
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
}
}
- 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
- Coverage Report:
- HTML report: coverage/lcov-report/index.html
- Console summary after running tests
- Detailed line-by-line coverage in HTML report
- Current Coverage Status:
- Function Coverage: 100%
- Line Coverage: 100%
- Branch Coverage: 100%
Best Practices
- Test Organization:
- Group related tests using describe
- Use clear, descriptive test names
- Follow the Arrange-Act-Assert pattern
- Test Isolation:
- Each test should be independent
- Clean up after each test
- Reset mocks between tests
- Test Coverage:
- Aim for meaningful coverage
- Focus on critical paths
- Test edge cases and error scenarios
- 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.