Lesson 58 of 81
A Server Component isn't a function you can render-and-assert on the way you would a client component. What to actually test, and which tool fits which layer.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
An App Router app has three genuinely different kinds of code, and they need three different testing approaches — reaching for one tool everywhere is how teams end up either under-tested or fighting their test runner.
Server Component logic — the data-fetching and transformation that happens inside an async Server Component — usually isn't worth rendering in a test at all. It runs on the server, has no client-side behavior, and most of its complexity is really just "take this data, shape it correctly." Pull that shaping logic into a plain, exported function and unit test the function directly; there's nothing React-specific left to test.
Route Handlers (route.ts files) are ordinary request-in, response-out functions — test them as integration tests, calling the exported GET/POST with a real Request and asserting on the real Response, no browser needed.
Client Components with real interactivity — a form, a toggle, anything with useState or an event handler — are what component-testing tools are actually built for. That's where React Testing Library or a browser-based runner earns its keep.
Pull the shaping logic out of the Server Component so it's a plain function:
// app/dashboard/get-summary.ts
export function summarize(orders: { total: number }[]) {
return {
count: orders.length,
revenue: orders.reduce((sum, o) => sum + o.total, 0),
};
}
// app/dashboard/get-summary.test.ts
import { describe, it, expect } from "vitest";
import { summarize } from "./get-summary";
describe("summarize", () => {
it("sums revenue across orders", () => {
expect(summarize([{ total: 10 }, { total: 25 }])).toEqual({
count: 2,
revenue: 35,
});
});
});
page.tsx just calls summarize(await getOrders()) and renders the result — there's no separate test for the page, because there's no logic left in it worth testing in isolation.
A Route Handler gets tested by calling it directly, no server or browser required:
// app/api/orders/route.test.ts
import { describe, it, expect } from "vitest";
import { GET } from "./route";
describe("GET /api/orders", () => {
it("returns 200 with an array", async () => {
const res = await GET(new Request("http://localhost/api/orders"));
expect(res.status).toBe(200);
expect(Array.isArray(await res.json())).toBe(true);
});
});
async Server Component the normal way. RTL's render() expects a synchronous component; an async function Page() isn't one. This is exactly why pulling logic into a plain function is the fix, not a workaround for a missing RTL feature — the component itself often has nothing left worth unit testing.GET directly skips real HTTP, middleware, and the actual deployed runtime — it proves the handler's own logic is correct, not that the full request pipeline in front of it is wired up right. Both are worth having.fetch in a Server Component test usually means you're testing the wrong layer. If a test needs to mock the network to check a rendering decision, that decision — not the fetch — is what should have been pulled into a plain, testable function in the first place.The reason these three testing approaches don't collapse into one is the same server/client split this track's earliest lessons on the file-convention model cover — a Server Component runs only on the server and produces no client-side behavior to test, a Client Component runs in the browser and is exactly what component-testing tools target, and a Route Handler is neither, just a function. Knowing which category a piece of code falls into is what tells you which tool to reach for.