Jest Configuration and Usage
Jest is a delightful JavaScript testing framework with a focus on simplicity. This document covers how Jest is configured and used in this project for unit and integration testing.
Overview
- Version: 29.x
- Environment: jsdom (for simulating browser environment)
- Extensions:
ts-jest
: TypeScript support for Jest@testing-library/react
: React component testing@testing-library/jest-dom
: Custom Jest matchers for DOM testing@testing-library/user-event
: Simulating user events
Configuration
The project should have a Jest configuration file at the project root. If not created yet, you can create a jest.config.js
file with the following configuration:
const nextJest = require('next/jest');
const createJestConfig = nextJest({
// Provide the path to your Next.js app
dir: './',
});
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
// Handle module aliases
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
'^@/app/(.*)$': '<rootDir>/app/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
},
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
transform: {
// Use ts-jest to handle TypeScript files
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
coveragePathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/.next/',
'<rootDir>/coverage/',
],
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config
module.exports = createJestConfig(customJestConfig);
Additionally, create a jest.setup.js
file that imports the testing library extensions:
// jest.setup.js
import '@testing-library/jest-dom';
Default Commands
The following npm scripts are available for testing:
-
Run all tests: Runs all test files
npm test
-
Watch mode: Runs tests in watch mode, which will rerun tests when files change
npm run test:watch
-
Coverage report: Generates a test coverage report
npm run test:coverage
Writing Tests
1. Unit Tests for Utility Functions
Place test files alongside the files they are testing with a .test.ts
or .test.tsx
extension.
// utils/formatDate.test.ts
import { formatDate } from './formatDate';
describe('formatDate', () => {
it('formats date correctly', () => {
const date = new Date('2023-01-01');
expect(formatDate(date)).toBe('01/01/2023');
});
it('handles invalid dates', () => {
expect(formatDate(null)).toBe('');
});
});
2. Component Testing with React Testing Library
// components/button.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('calls onClick when clicked', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
3. Testing Hooks
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Test Directory Structure
The recommended directory structure for tests:
project/
├── components/
│ ├── Button.tsx
│ └── Button.test.tsx
├── lib/
│ ├── utils.ts
│ └── utils.test.ts
├── app/
│ └── __tests__/
│ └── page.test.tsx
└── __tests__/
└── integration/
└── form-submission.test.tsx
Mocking
Mocking Modules
// Mocking a module
jest.mock('next/router', () => ({
useRouter: () => ({
push: jest.fn(),
pathname: '/',
query: {},
}),
}));
Mocking API Calls
// Mocking fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'mocked data' }),
ok: true,
})
) as jest.Mock;
Testing Asynchronous Code
// Testing promises
it('fetches data asynchronously', async () => {
const data = await fetchData();
expect(data).toEqual({ success: true });
});
// Testing with waitFor
it('updates after async operation', async () => {
render(<AsyncComponent />);
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
});
Best Practices
- Use Testing Library queries appropriately: Prefer queries that reflect how users interact with your application (getByRole, getByLabelText) over implementation details (getByTestId)
- Keep tests simple: Each test should verify one specific behavior
- Avoid testing implementation details: Focus on testing behavior, not implementation
- Use the right assertions: Be specific about what you're testing
- Organize tests with describe blocks: Group related tests together
- Follow the AAA pattern: Arrange, Act, Assert
- Run tests in CI: Include tests in your continuous integration workflow