Next.js
Testing Next.js Apps
Build reliable tests for your Next.js components, API routes, and pages to catch bugs before users do
Testing Next.js apps goes beyond regular React testing. You have server-side rendering, API routes, and static generation to verify. Unlike plain React apps, your Next.js tests need to cover both client-side components and server-side functionality. Think of testing like quality control in a factory. Each part gets checked individually. Then you test how they work together. A car factory tests the brakes separately, then tests the whole car on the road. Your NewsWave site needs the same thoroughness. Modern web apps ship broken features daily. Users expect articles to load instantly. Search functionality must return results. Newsletter signups should work flawlessly. One broken component ruins the entire experience.Understanding Next.js Testing Layers
Next.js apps have multiple layers that need testing. Each layer serves a different purpose and requires different tools. You cannot test everything with one approach. Unit tests check individual functions and components in isolation. They run fast and catch logic errors. Think of testing a single LEGO brick for cracks. Integration tests verify multiple components working together. They catch bugs that happen when components interact. Like testing if two LEGO bricks connect properly. End-to-end tests simulate real user behavior from start to finish. They test the complete user journey. Like building the entire LEGO castle and making sure it stands. API route testing validates your backend endpoints. These tests ensure your data flows correctly between frontend and backend. Newsletter signups must save to the database. Article fetching must return proper JSON.Unit Tests
Test individual components and functions in isolation
Integration Tests
Test components working together
E2E Tests
Test complete user journeys
API Tests
Test backend endpoints and data flow
Setting Up Jest and React Testing Library
Jest handles the test running and assertions. React Testing Library helps you test components like a user would interact with them. Together they form the foundation of Next.js testing. React Testing Library changed how developers think about testing. Instead of testing implementation details, you test behavior. You click buttons, fill forms, and check what appears on screen. Jest comes with powerful features for Next.js. It can mock modules, simulate different environments, and generate coverage reports. The combination gives you everything needed for comprehensive testing.# Install testing dependencies for NewsWave
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# Install additional helpers for Next.js testing
npm install --save-dev @testing-library/user-event jest-environment-jsdomWhat just happened?
You installed Jest for running tests, React Testing Library for component testing, and jest-dom for additional matchers. These tools work together to create a complete testing environment. Try this: Check your package.json devDependencies section.
// jest.config.js - Configure Jest for Next.js testing
const nextJest = require('next/jest')
// Create Jest config that works with Next.js
const createJestConfig = nextJest({
dir: './', // Path to Next.js app
})
// Custom Jest configuration
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom', // Browser-like environment
}// jest.setup.js - Global test configuration
import '@testing-library/jest-dom' // Adds custom matchers like toBeInTheDocument
// Mock Next.js router for all tests
jest.mock('next/router', () => ({
useRouter: () => ({
push: jest.fn(),
pathname: '/',
query: {},
}),
}))// package.json - Add testing scripts
{
"scripts": {
"test": "jest", // Run all tests once
"test:watch": "jest --watch", // Run tests in watch mode
"test:coverage": "jest --coverage" // Run tests with coverage report
}
}Writing Component Tests
Component testing verifies that your React components render correctly and handle user interactions. You test what users see and do, not internal implementation details. The key principle: test behavior, not implementation. Users do not care about state variables or function names. They care that clicking a button shows content or submitting a form saves data. Start with the simplest component. A NewsWave article card should display the title, author, and publish date. Users should be able to click it to navigate to the full article.// components/ArticleCard.js - Simple article card component
export default function ArticleCard({ article }) {
return (
<div className="article-card">
<h3>{article.title}</h3>
<p>By {article.author}</p>
<span>{article.publishedAt}</span>
</div>
)
}// __tests__/ArticleCard.test.js - Test the article card component
import { render, screen } from '@testing-library/react'
import ArticleCard from '../components/ArticleCard'
// Mock article data for testing
const mockArticle = {
title: 'Next.js Testing Best Practices',
author: 'Jane Developer',
publishedAt: 'March 10, 2024'
}// Test that component renders article information
test('displays article title, author, and date', () => {
render(<ArticleCard article={mockArticle} />)
// Check if title appears on screen
expect(screen.getByText('Next.js Testing Best Practices')).toBeInTheDocument()
// Check if author appears on screen
expect(screen.getByText('By Jane Developer')).toBeInTheDocument()
// Check if date appears on screen
expect(screen.getByText('March 10, 2024')).toBeInTheDocument()
})What just happened?
You created a test that renders the ArticleCard component with mock data and verifies the content appears correctly. The test uses screen.getByText to find elements and expects them to be in the document. Try this: Run the test and watch it pass.
// Test user interaction with the search component
import userEvent from '@testing-library/user-event'
test('search form submits with user input', async () => {
const user = userEvent.setup() // Setup user event simulation
const mockOnSearch = jest.fn() // Mock function to track calls
render(<SearchForm onSearch={mockOnSearch} />)
// Find search input and button
const searchInput = screen.getByPlaceholderText('Search articles...')
const searchButton = screen.getByRole('button', { name: /search/i })
} // Simulate user typing in search box
await user.type(searchInput, 'Next.js testing')
// Simulate user clicking search button
await user.click(searchButton)
// Verify the search function was called with correct term
expect(mockOnSearch).toHaveBeenCalledWith('Next.js testing')
expect(mockOnSearch).toHaveBeenCalledTimes(1)
})Testing API Routes
API route testing ensures your backend endpoints return correct data and handle errors properly. Unlike component tests, these focus on HTTP requests, database operations, and JSON responses. Next.js API routes run on the server, but you can test them without starting a full server. You create mock requests, call your route handlers directly, and verify the responses. Testing API routes catches critical bugs before deployment. A broken newsletter signup loses subscribers. A faulty search endpoint frustrates users. Database connection errors crash your entire app.// pages/api/newsletter.js - API route for newsletter signups
export default async function handler(req, res) {
// Only allow POST requests for newsletter signup
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
const { email } = req.body
// Validate email format
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Valid email required' })
} try {
// Simulate saving email to database
await saveEmailToDatabase(email)
// Return success response
res.status(200).json({
success: true,
message: 'Subscribed successfully'
})
} catch (error) {
// Handle database errors
res.status(500).json({ error: 'Subscription failed' })
}
}// __tests__/api/newsletter.test.js - Test newsletter API route
import handler from '../../pages/api/newsletter'
import { createMocks } from 'node-mocks-http'
// Mock the database function
jest.mock('../../lib/database', () => ({
saveEmailToDatabase: jest.fn()
}))
describe('/api/newsletter', () => {
test('successfully subscribes valid email', async () => {
// Create mock request and response objects
const { req, res } = createMocks({
method: 'POST',
body: { email: 'user@newswave.com' }
}) // Call the API route handler
await handler(req, res)
// Verify successful response
expect(res._getStatusCode()).toBe(200)
expect(JSON.parse(res._getData())).toEqual({
success: true,
message: 'Subscribed successfully'
})
})
}) test('rejects invalid email addresses', async () => {
const { req, res } = createMocks({
method: 'POST',
body: { email: 'invalid-email' } // Missing @ symbol
})
await handler(req, res)
// Verify error response
expect(res._getStatusCode()).toBe(400)
expect(JSON.parse(res._getData())).toEqual({
error: 'Valid email required'
})
}) test('rejects non-POST requests', async () => {
const { req, res } = createMocks({
method: 'GET' // Wrong HTTP method
})
await handler(req, res)
// Verify method not allowed response
expect(res._getStatusCode()).toBe(405)
expect(JSON.parse(res._getData())).toEqual({
error: 'Method not allowed'
})
})What just happened?
You tested your API route with different scenarios including success, validation errors, and wrong HTTP methods. The createMocks function simulates HTTP requests without starting a server. Try this: Add a test for database connection failures.
Testing Pages and Routing
Page testing verifies that your Next.js pages render correctly with proper data and handle routing scenarios. Pages combine components, data fetching, and navigation logic that all need testing. Next.js pages have unique challenges. They might usegetStaticProps, getServerSideProps, or dynamic routing. Each pattern requires different testing approaches.
Page tests often need more setup than component tests. You must mock Next.js router, simulate data fetching functions, and handle dynamic routes. But the effort pays off by catching integration bugs.
// pages/articles/[slug].js - Dynamic article page
import { useRouter } from 'next/router'
export default function ArticlePage({ article }) {
const router = useRouter()
// Show loading state while router loads
if (router.isFallback) {
return <div>Loading article...</div>
}
// Show error if article not found
if (!article) {
return <div>Article not found</div>
} return (
<article>
<h1>{article.title}</h1>
<p>By {article.author} on {article.publishedAt}</p>
<div>{article.content}</div>
<button onClick={() => router.back()}>
Back to Articles
</button>
</article>
)
}// __tests__/pages/article-page.test.js - Test article page
import { render, screen } from '@testing-library/react'
import { useRouter } from 'next/router'
import ArticlePage from '../../pages/articles/[slug]'
// Mock Next.js router
jest.mock('next/router', () => ({
useRouter: jest.fn()
}))
const mockPush = jest.fn()
const mockBack = jest.fn()describe('ArticlePage', () => {
beforeEach(() => {
// Reset router mock before each test
useRouter.mockReturnValue({
query: { slug: 'test-article' },
push: mockPush,
back: mockBack,
isFallback: false
})
})
test('displays article content when loaded', () => {
const mockArticle = {
title: 'Understanding Next.js Testing',
author: 'Test Author',
publishedAt: 'March 2024',
content: 'Article content here...'
} render(<ArticlePage article={mockArticle} />)
// Verify article content appears
expect(screen.getByText('Understanding Next.js Testing')).toBeInTheDocument()
expect(screen.getByText('By Test Author on March 2024')).toBeInTheDocument()
expect(screen.getByText('Article content here...')).toBeInTheDocument()
})
test('shows loading state during fallback', () => {
// Mock fallback state
useRouter.mockReturnValue({
query: {},
isFallback: true
}) render(<ArticlePage article={null} />)
// Verify loading message appears
expect(screen.getByText('Loading article...')).toBeInTheDocument()
})
test('handles back button click', async () => {
const user = userEvent.setup()
render(<ArticlePage article={mockArticle} />)
// Click back button
await user.click(screen.getByText('Back to Articles'))
// Verify router.back was called
expect(mockBack).toHaveBeenCalledTimes(1)
})
})End-to-End Testing with Playwright
End-to-end testing simulates real user workflows from start to finish. These tests run in actual browsers and verify that your entire application works together correctly. Playwright has become the modern choice for E2E testing. It supports multiple browsers, handles modern web features, and provides reliable test execution. Unlike older tools, Playwright rarely produces flaky tests. E2E tests catch bugs that unit and integration tests miss. They verify that your NewsWave site actually works for real users. Can visitors browse articles? Does search return results? Can users subscribe to newsletters?# Install Playwright for end-to-end testing
npm install --save-dev @playwright/test
# Initialize Playwright configuration
npx playwright install// e2e/newsletter-signup.spec.js - Test complete newsletter signup flow
import { test, expect } from '@playwright/test'
test('user can subscribe to newsletter', async ({ page }) => {
// Navigate to NewsWave homepage
await page.goto('http://localhost:3000')
// Verify page loaded correctly
await expect(page).toHaveTitle(/NewsWave/)
// Find newsletter signup form
const emailInput = page.locator('input[type="email"]')
const subscribeButton = page.locator('button:has-text("Subscribe")') // Fill in email address
await emailInput.fill('test@newswave.com')
// Click subscribe button
await subscribeButton.click()
// Wait for success message to appear
await expect(page.locator('text=Successfully subscribed')).toBeVisible()
// Verify email input is cleared after successful signup
await expect(emailInput).toHaveValue('')
})// e2e/article-browsing.spec.js - Test article discovery features
test('user can browse and search articles', async ({ page }) => {
await page.goto('http://localhost:3000')
// Click on first article
await page.click('text=Breaking: Next.js 15 Released')
// Verify navigation to article page
await expect(page).toHaveURL(/\/articles\//)
await expect(page.locator('h1')).toContainText('Breaking: Next.js 15 Released')
// Go back to homepage
await page.goBack() // Test search functionality
await page.fill('input[placeholder="Search articles..."]', 'Next.js')
await page.click('button:has-text("Search")')
// Wait for search results
await expect(page.locator('.search-results')).toBeVisible()
await expect(page.locator('.article-card')).toContainText('Next.js')
})What just happened?
You created end-to-end tests that simulate real user interactions in a browser. These tests verify complete workflows from homepage to article reading to newsletter signup. Try this: Run the tests and watch Playwright control the browser automatically.
Testing Best Practices
Good tests share common characteristics. They run fast, produce consistent results, and clearly communicate what they test. Bad tests slow development, break randomly, and confuse developers. The testing pyramid guides your testing strategy. Write many unit tests, fewer integration tests, and just enough E2E tests. Unit tests catch bugs early and run in seconds. E2E tests catch critical workflows but take minutes. Test behavior, not implementation. Users care about clicking buttons and seeing results. They do not care about internal state management or function calls. Focus tests on what users experience.Common Testing Mistakes
Testing implementation details instead of behavior leads to brittle tests that break when code changes. Testing too many things in one test makes failures hard to debug. Not testing edge cases like empty states or error conditions leaves bugs undiscovered.
// __tests__/helpers/test-utils.js - Reusable testing utilities
import { render } from '@testing-library/react'
import { RouterContext } from 'next/dist/shared/lib/router-context'
// Custom render function with router mock
export function renderWithRouter(component, router = {}) {
const defaultRouter = {
route: '/',
pathname: '/',
query: {},
asPath: '/',
push: jest.fn(),
...router
} return render(
<RouterContext.Provider value={defaultRouter}>
{component}
</RouterContext.Provider>
)
}
// Create mock articles for consistent testing
export const createMockArticle = (overrides = {}) => ({
id: '1',
title: 'Test Article Title',
author: 'Test Author',
publishedAt: '2024-03-15',
content: 'Test article content...',
...overrides
})// __tests__/components/ArticleList.test.js - Using helper functions
import { screen } from '@testing-library/react'
import { renderWithRouter, createMockArticle } from '../helpers/test-utils'
import ArticleList from '../../components/ArticleList'
test('displays list of articles', () => {
const articles = [
createMockArticle({ title: 'First Article' }),
createMockArticle({ title: 'Second Article', id: '2' })
]
renderWithRouter(<ArticleList articles={articles} />)
expect(screen.getByText('First Article')).toBeInTheDocument()
expect(screen.getByText('Second Article')).toBeInTheDocument()
})What just happened?
You created reusable test utilities that eliminate duplicate code and make tests more maintainable. Helper functions handle common setup like router mocking and test data creation. Try this: Use these helpers in all your component tests.
// package.json - Scripts for different testing scenarios
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage --watchAll=false",
"test:ci": "jest --ci --coverage --watchAll=false",
"test:e2e": "playwright test",
"test:all": "npm run test:ci && npm run test:e2e"
}
}Quiz
1. The NewsWave team wants to test their article search component. What should they focus on testing?
2. When testing NewsWave's newsletter API route, what tool helps create mock HTTP requests and responses?
3. What does the testing pyramid recommend for NewsWave's testing strategy?
Up Next: Security in Next.js
Protect your Next.js applications from common security vulnerabilities and implement authentication best practices.