Testing Server Code

Testing Server Components and Server Actions

Split a route so the part that fetches is its own component behind a <Suspense> boundary: the shell is then synchronous, and the asynchronous half is isolated.

app/products/page.tsxTSX
export async function ProductList() {
  const products = await getProducts();
  return <ul>{products.map((p) => <li key={p.sku}>{p.name}</li>)}</ul>;
}
export default function ProductsPage() {
  const busy = <p data-testid="loading">Checking inventory...</p>;
  return <main><h1>Products</h1>
    <Suspense fallback={busy}><ProductList /></Suspense></main>;
}

render(<ProductsPage />) finds the heading and the fallback. render(<ProductList />) does not: outside the server renderer, React 7,897 treats an async function component as a client component.

Output of 91
$ npx vitest run __tests__/async-jsx.test.tsx
stderr | rendering an async server component as JSX
<ProductList> is an async Client Component. Only Server Components can be async at the
moment. This error is often caused by accidentally adding `'use client'` to a module that
was originally written for the server.
 FAIL  TestingLibraryElementError: Unable to find an element with the text: Enamel mug
<body><div /></body>

No configuration fixes that, and the documentation agrees: "Vitest 102,989 currently does not support [async Server Components] ... we recommend using E2E tests for async components". What works is to call the component and render what it returns: render(await ProductList()) finds Enamel mug — $12.00 once the fetch is mocked (Mocking the API with MSW). The trick ends when a component reaches for the request, since cookies(), headers(), connection() and "use cache" read a store only a running server creates.

Server Actions

A Server Action is an exported async function, so a test can call it — up to the first framework call. addToCart validates a quantity, then calls revalidatePath('/cart'). Its rejection path passes at once; its success path does not:

Output of 91
 FAIL  __tests__/actions.test.ts > accepts a valid quantity
Error: Invariant: static generation store missing in revalidatePath /cart
 ❯ revalidate node_modules/next/src/server/web/spec-extension/revalidate.ts:135:11
 ❯ addToCart app/products/actions.ts:12:3

revalidatePath, revalidateTag, updateTag and redirect all expect that store. Replace the module with vi.mock('next/cache', () => ({ revalidatePath: vi.fn() })) and assert expect(revalidatePath).toHaveBeenCalledWith('/cart') instead — the more useful assertion anyway.