Handbooks / Next.js / Chapter 8
Testing and Monitoring
7 pages · ~5 min✓ Reviewed
Builds on Authentication Patterns. Next up: Optimization and Deployment.
Testing Layers and the Mental Model
1. The Pyramid
- UnitVitest/Jest + RTL, many, fast
- Integrationa few
- E2EPlaywright, critical flows only
2. Jest vs Vitest
| Vitest | Jest | |
|---|---|---|
| Built on | Vite, ESM-first | next/jest, SWC transform |
| Mock fn | vi.fn | jest.fn |
| API | describe/it/expect | same |
3. What to Test and Mock
- Async Server Components: E2E, RTL cannot render them
- Or unit test their extracted pure logic
- Test what the user sees: roles and text
- Not state, class names or internal calls
- Mock network, DB,
next/navigation - Never mock the component under test
javascript
vi.mock('next/navigation') render(<Save />) screen.getByRole('button', { name: /save/i })
mock the edge, query by role
Boundary rule
Mock what is outside the component, never the component itself.
4. Interview Point
- RTL and async Server Components? It cannot render them; use E2E or extracted logic.
- What do you mock? Network, DB,
next/navigation; never the unit under test.
Unit Tests with Jest or Vitest and RTL
1. Setup
| Jest | Vitest | |
|---|---|---|
| Install | npm i -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom | npm i -D vitest @vitejs/plugin-react jsdom @testing-library/react |
| Config | jest.config.ts with createJestConfig from next/jest | vitest.config.mts with plugins [react()] |
| DOM | testEnvironment: 'jsdom' | test.environment: 'jsdom' |
2. Test Pattern
render(<Comp />), then query with screen.getByRole- Assert with jest-dom matchers like
toBeInTheDocument() - Use userEvent.setup() and
await user.click(...), notfireEvent - Async UI: findBy* or
waitFor
tsx
render(<Save />) const user = userEvent.setup() await user.click( screen.getByRole('button', { name: /save/i })) expect(await screen.findByText('Saved')) .toBeInTheDocument()
query by role and name, like a user
Testing Server Components and Server Actions
1. What RTL Can Render
| Target | Unit test? |
|---|---|
| Sync Server Component | Renders fine with RTL |
| Async Server Component | Cannot render: use Playwright |
| Extracted async function | Yes, mock fetch |
- Move the fetch/query out into a plain async function
- Unit test that function with a mocked
fetch
2. Server Actions
- An action is just an async function marked
'use server' - Import it, call it with a
FormData - Mock
next/headers,next/cache,next/navigationwithvi.mock/jest.mock - Real
redirectthrows NEXT_REDIRECT, so mock it and assert the call
Playwright End-to-End Testing
1. Setup
typescript
export default defineConfig({
testDir: './e2e',
use: { baseURL: 'http://localhost:3000' },
webServer: { command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI },
})playwright.config.ts, scaffolded by npm init playwright@latest
- Run against the production build, not
next dev - Catches real caching and rendering behavior
2. Locators and Assertions
- Locators auto-wait until the element is actionable
- Assert with
expect; web-first assertions retry
typescript
await page.getByRole('link', { name: 'Docs' }).click() await expect(page).toHaveURL(/docs/) await expect(page.getByRole('heading')).toBeVisible()
3. Commands
| Command | Does |
|---|---|
npx playwright test | run all tests |
--ui |
instrumentation.ts, Error Boundaries and Reporting
1. instrumentation.ts
- At project root (or
src/); register() runs once per server instance at startup - Use it to init OpenTelemetry or an error SDK
- Guard runtime-specific code with
NEXT_RUNTIME, or Edge bundling breaks - onRequestError reports render, route handler, action and middleware errors
typescript
export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./instrumentation-node') } } export async function onRequestError(err, request, context) { await report(err, request.path, context.routePath) }
report() = your Sentry-style call
2. Error Boundaries
| File | Replaces | Must |
|---|---|---|
error.tsx | segment children | 'use client', gets {error, reset} |
global-error.tsx | root layout | render own <html> and <body> |
Web Vitals and Testing Cheat Sheet
1. Core Web Vitals
| Metric | Good |
|---|---|
| LCP | under 2.5 s |
| INP | under 200 ms |
| CLS | under 0.1 |
- Other metrics: FCP, TTFB
- Lab: Lighthouse, a single synthetic run
- Field: CrUX and your own reporting, what counts
2. Report Them
'use client'component, placed in the root layout- POST with
navigator.sendBeaconto your analytics
tsx
'use client' import { useReportWebVitals } from 'next/web-vitals' export function Vitals() { useReportWebVitals((m) => navigator.sendBeacon('/api/vitals', JSON.stringify(m))) return null }
Check yourself
RTL render(<Page />) fails when Page is an async Server Component. What do you do?
- RTL can't render async Server Components.
- Extract the fetch into a plain async function and unit test it with mocked fetch.
- Cover the page itself with Playwright.
A Server Action ends with redirect('/done'). You call it in a unit test with vi.mock('next/navigation'). What do you assert?
- The real
redirectthrows NEXT_REDIRECT. - With the mock, assert
redirectwas called with '/done'.
app/dashboard/layout.tsx throws. app/dashboard/error.tsx exists. Does it catch it?
- No. error.tsx wraps its children, not the same-level layout.
- The parent segment's error.tsx or global-error.tsx catches it.