19. Testing
Introduction
Testing is a cornerstone of reliable software development. In the Next.js ecosystem, a well‑tested application gives developers confidence when refactoring, adding features, or deploying to production. This chapter walks you through the three main testing layers commonly used in Next.js projects: unit testing, component testing, and end‑to‑end (E2E) testing. For each layer we introduce the most popular tools—Jest, Vitest, React Testing Library, Playwright, and Cypress—show how to configure them, write effective tests, and integrate them into a continuous integration (CI) pipeline.
Why Testing Matters in Next.js
- Early bug detection: Unit tests catch logic errors before they propagate.
- Component confidence: React Testing Library ensures UI behaves as expected from the user’s perspective.
- User‑flow validation: Playwright and Cypress simulate real browsers, catching integration and routing issues.
- Documentation: Tests serve as living documentation of intended behavior.
- Refactoring safety: A solid test suite lets you upgrade Next.js, React, or dependencies with minimal risk.
Setting Up the Testing Environment
Project Initialization
Start with a fresh Next.js app (or use an existing one).
npx create-next-app@latest my-next-app --typescript
Change into the project folder and install the testing dependencies.
Jest Configuration
Jest is the traditional test runner for JavaScript/TypeScript projects. Install the core packages:
npm i -D jest @types/jest ts-jest
Create a jest.config.js file at the project root:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: {
// Handle CSS imports (with CSS modules)
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
// Handle image imports
'\\.(gif|ttf|eot|svg|png)$': '/__mocks__/fileMock.js',
},
setupFilesAfterEnv: ['/jest.setup.js'],
};
Install identity-obj-proxy for CSS mocking and create a simple file mock:
npm i -D identity-obj-proxy
// __mocks__/fileMock.js
module.exports = 'test-file-stub';
Add a basic Jest setup file to extend matchers:
// jest.setup.js
import '@testing-library/jest-dom';
Vitest Configuration (Alternative)
Vitest is a Vite‑native test runner that offers blazing‑fast hot module replacement (HMR) during test development. Install Vitest and related plugins:
npm i -D vitest @vitest/ui happy-dom
Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
setupFiles: './vitest.setup.ts',
// Alias for Next.js absolute imports
alias: {
'@/*': ['./*'],
},
},
});
Create a setup file to import DOM matchers:
// vitest.setup.ts
import '@testing-library/jest-dom';
Unit Testing with Jest and Vitest
Unit tests focus on pure functions, utility modules, and small pieces of business logic. They are fast, deterministic, and easy to maintain.
Testing Utility Functions
Consider a utility that formats a price:
// lib/formatPrice.ts
export function formatPrice(amount: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
}
Write a Jest test:
// __tests__/formatPrice.test.ts
import { formatPrice } from '../lib/formatPrice';
describe('formatPrice', () => {
test('formats a whole number as USD', () => {
expect(formatPrice(1000)).toBe('$1,000.00');
});
test('accepts a custom currency', () => {
expect(formatPrice(2500, 'EUR')).toBe('€2,500.00');
});
test('handles decimal values', () => {
expect(formatPrice(1234.5)).toBe('$1,234.50');
});
});
The same test works with Vitest; just run
Testing Best Practices
- Test behavior, not implementation: Focus on what the code does, not how
- Keep tests independent: Each test should be self-contained
- Use descriptive names: Test names should describe the expected behavior
- Follow AAA pattern: Arrange, Act, Assert
- Test edge cases: Include boundary conditions and error scenarios
Test Coverage
While 100% coverage is not always practical, aim for high coverage on critical paths. Coverage reports help identify untested code. Use tools like Istanbul or c8 to measure and track coverage over time.
Integration vs Unit Tests
A balanced test suite includes both unit tests (fast, isolated, testing individual functions) and integration tests (slower, testing component interactions). The testing pyramid suggests more unit tests than integration tests, with even fewer end-to-end tests.
Continuous Testing
Integrate tests into your CI/CD pipeline to catch regressions early. Run tests on every commit and pull request. Use test parallelization to reduce feedback time.