Module P-9·24 min read

Testing Server Components that call cookies()/headers() outside request context, AsyncLocalStorage-aware test wrappers, mocking the Data Cache layer for deterministic tests, testing optimistic UI race conditions, E2E with Playwright, and the five test patterns that catch real production bugs.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

P-9 — Testing Next.js Applications

Who this is for: Engineers who write tests for regular React apps but find that Server Components, Route Handlers, and Server Actions break their existing test setup in confusing ways. This module covers the correct testing strategy for each piece of the Next.js App Router, from unit tests that actually work to E2E flows that catch what unit tests miss.


Why Next.js Testing Is Harder Than You'd Expect

Testing a plain React component is straightforward. Render it, fire some events, assert on the output. The mental model is simple because components are synchronous functions that return JSX.

Server Components break this model in three ways. First, they are async functions — async function Page() — and React Testing Library's render() doesn't handle async component trees without extra ceremony. Second, they use server-only APIs: cookies(), headers(), redirect(), and notFound() from next/headers and next/navigation. These throw at import time in a Jest environment unless you mock them. Third, modules that contain 'use server' or import from next/server often fail to load in Jest's Node.js environment because they reference APIs that only exist in a Next.js server context.

The first instinct is to mock everything aggressively. That instinct is right for unit tests. But it also means your unit tests are testing your component in isolation from the actual Next.js runtime, which is why a passing unit test suite and a broken production app can coexist. You need both unit tests and E2E tests, and knowing which to use where is the main skill this module builds.


Unit Testing Server Components

The correct approach for testing an async Server Component with Jest and React Testing Library is to await the component before rendering:

tsx

The key insight: instead of passing <BlogPage /> to render(), you call BlogPage() directly as an async function, await it to get the resolved JSX, and then pass that JSX to render(). This sidesteps the async component rendering problem entirely.

It feels slightly wrong — you're not rendering the component in the traditional sense — but it works and it correctly exercises your component's logic. The alternative, patching React's internals to handle async components in tests, is fragile and constantly breaks across React versions.


Mocking next/navigation

Any component that calls useRouter(), usePathname(), or useSearchParams() will fail in tests without a mock. The standard pattern:

ts

Put this in a jest.setup.ts file and import it in your Jest config under setupFilesAfterFramework, and every test gets it automatically. If you need different behavior per test, you can reach into the mock: (usePathname as jest.Mock).mockReturnValue('/dashboard').


Mocking next/headers

cookies() and headers() from next/headers work similarly. They throw if called outside a Next.js request context, so they need mocks in tests:

ts

For tests that care about specific cookie values, override in the individual test:

ts

Testing Route Handlers

Route Handlers in Next.js 15 are just functions that take a Request and return a Response. You can test them without a running server by constructing a NextRequest and calling the handler directly:

ts

This is clean and fast. No supertest, no server setup, no port management. The handler is a pure async function and you test it like one. Mock your data layer with jest.mock and you can test all the edge cases — malformed input, missing fields, database errors — without touching a real database.


Testing Server Actions

Server Actions are async functions. In tests, you call them as async functions. That's it:

ts

The 'use server' directive is a build-time transformation, not a runtime behavior. In tests, it's ignored. Your action is a function; test it like one.


Vitest as a Jest Alternative

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.