Skip to main content
Testing & Quality · Blog 33

Next.js 16 Testing Server Components, Client Components and Server Actions

Match Vitest, React Testing Library and Playwright to the App Router boundary they can verify reliably—from pure rules to complete server-driven browser flows.

Server, interactive browser component and secure action pipeline connected by successful tests

In Blog #31, we set up Vitest and React Testing Library for unit and component tests. In Blog #32, we added Playwright for complete browser workflows. Now we combine those tools into a practical Next.js component testing strategy for the three most important App Router building blocks: Server Components, Client Components and Server Actions.

These building blocks do not share one runtime or one risk profile. A Server Component may resolve data before HTML reaches the browser. A Client Component owns state, effects and event handlers. A Server Action crosses back to trusted server code to validate input, authorize a user, write data and refresh the interface. No single test type is best for all of them.

How Should You Test Server Components, Client Components and Server Actions in Next.js?

Testing Strategy at a Glance

Begin with the lowest-cost test that can produce reliable evidence. Move outward when behavior depends on request context, rendering, navigation, cache invalidation or persistence. “Lowest” does not mean “always unit.” For an async page whose contract is visible HTML, a small Playwright check may be simpler and more honest than a complex imitation of the server runtime.

Diagram 1: App Router testing mapTest each concern at the lowest level that gives reliable confidence.
Next.js App Router
Pure logicVitest
Client ComponentsVitest + RTL
Async Server ComponentsPlaywright
Server ActionsPure logic: Vitest
Full flow: Playwright
Code or behaviorBest starting test
Pure validation helperVitest
Price calculationVitest
Client button behaviorVitest + RTL
Client form interactionVitest + RTL
Async Server Component renderingPlaywright
Server Action validation helperVitest
Full Server Action submissionPlaywright
Redirect after mutationPlaywright
Cache revalidation visible to a userPlaywright or integration test
Authorization ruleUnit/integration plus E2E for a critical path

Why App Router Testing Is Different

Classic client-heavy React puts much of the application logic inside a browser component. App Router splits work across server and client boundaries. Server Components render on the server and can access server-side data. Client Components add interactivity. Server Actions are server functions invoked from forms or client code. Testing should follow those execution boundaries rather than pretending everything is a jsdom component.

Diagram 2: Execution boundaryA request can cross server rendering, browser interactivity and a server-side mutation.
Request
Server Componentdata · auth · render
Client Componentstate · events · browser APIs
Server Actionvalidate · mutate

Server Components vs Client Components vs Server Actions

Diagram 3: Three different responsibilitiesThe best test changes with the runtime and responsibility of each building block.

Server Component

  • Runs on the server
  • Can fetch data
  • No browser event handlers

Client Component

  • Interacts in the browser
  • Owns state and effects
  • Handles user events

Server Action

  • Runs a server mutation
  • Validates and authorizes
  • Writes, redirects or revalidates

Choose the Right Test Level

Unit tests

Unit tests are ideal for deterministic calculations, parsers, validation rules and permission decisions. They should be fast, explicit and independent of the framework runtime. A unit test cannot establish that a browser form sent the expected request or that a revalidated page is fresh.

Component tests

Component tests render a small React UI in a DOM-like environment. They are strongest for Client Component props, state, conditional content, accessibility and interaction. jsdom is not a full browser and does not recreate the React Server Components protocol.

Integration tests

Integration tests connect meaningful layers: a service to a disposable database, a schema to a use case, or an authorization policy to a repository. Select boundaries based on risk. An integration suite that secretly connects to production is a dangerous deployment script, not a test.

E2E tests

End-to-end tests run a real browser against a running application. They cover routing, server rendering, JavaScript hydration, Server Action submission, redirect behavior and the final UI. They cost more to run, so reserve them for contracts whose value depends on those integrated layers.

Diagram 4: Confidence by test levelFocused checks are numerous and fast; complete browser flows are fewer and broader.
E2Euser journey
Integrationservices and data
Unit + componentlogic and UI behavior

Testing Pure Business Logic

Extract logic when it represents a reusable business rule—not merely to make a line testable. A calculation without request, database or React dependencies becomes a stable unit:

// calculate-total.ts
export function calculateTotal(price: number, quantity: number) {
  return price * quantity
}

// calculate-total.test.ts
import { describe, expect, it } from 'vitest'
import { calculateTotal } from './calculate-total'

describe('calculateTotal', () => {
  it('calculates the total', () => {
    expect(calculateTotal(25, 2)).toBe(50)
  })
})

This test is fast, deterministic and framework-independent. Add edge cases required by the domain, such as negative quantities or currency rounding, rather than chasing lines. Keep orchestration in the action when extraction would create a fragmented architecture with no reuse or clearer contract.

Testing Synchronous Server Components

A synchronous, dependency-light Server Component can be treated like rendered React output when the installed setup supports it. Current Next.js Vitest guidance demonstrates unit testing a simple page component, but that does not mean every server dependency works inside jsdom. Keep the example honest:

type GreetingProps = { name: string }

export default function Greeting({ name }: GreetingProps) {
  return <h1>Hello {name}</h1>
}

it('renders the supplied name', () => {
  render(<Greeting name="Ada" />)
  expect(screen.getByRole('heading', { name: 'Hello Ada' })).toBeVisible()
})

If the component imports request-only APIs, a server-only SDK or a database client, do not keep expanding mocks until it resembles production. Test pure collaborators and move the integrated rendering assertion to an appropriate runtime.

Testing Async Server Components

An async Server Component may fetch data, query a database, read request context, check authentication and render server-derived output. Current Next.js guidance says some tools do not fully support async Server Components and recommends E2E testing over unit testing for them. That limitation is important: do not call an async component as if it were an ordinary browser component and present a passing artificial test as framework confidence.

Diagram 5: Async Server Component strategySplit verifiable concerns without distorting the application solely for testing.
Async Server Component
Pure logicVitest
Data/service layerIntegration test
Rendered UIPlaywright
Request contextIntegration or E2E

A browser test should use a real route from the application, not a route copied from a tutorial. For example, if the audited app actually has /products and a Products heading:

test('server-rendered products are visible', async ({ page }) => {
  await page.goto('/products')
  await expect(page.getByRole('heading', { name: /products/i }))
    .toBeVisible()
})

Prefer deterministic seed data or a controlled service in the test environment. A production catalogue that changes during the run makes the assertion ambiguous.

Server data, an interactive browser and a protected database connected by verified execution steps
Server rendering, client interaction and protected mutation belong to one user experience, but each boundary deserves the right kind of test.

Testing Client Components

Client Components are natural React Testing Library subjects when they manage toggles, dialogs, tabs, local forms, state, effects or browser events. Test through roles, labels and visible outcomes. Avoid asserting a hook’s internal value, implementation-specific class or private callback sequence.

'use client'
import { useState } from 'react'

export function ToggleDetails() {
  const [open, setOpen] = useState(false)
  return <>
    <button onClick={() => setOpen(!open)}>Toggle details</button>
    {open && <p>More information</p>}
  </>
}
const user = userEvent.setup()
render(<ToggleDetails />)

await user.click(
  screen.getByRole('button', { name: /toggle details/i })
)

expect(screen.getByText('More information')).toBeVisible()

userEvent.setup() is the current recommended style for user-event sessions. Await interactions because they can trigger asynchronous browser-like behavior. Query a control the way a user or assistive technology discovers it.

Diagram 6: Client Component testDrive public behavior and assert the visible contract.
Render component
Find control
User interaction
Visible UI assertion

Testing Forms, useActionState and useFormStatus

Forms need two complementary test levels. At component level, verify fields, labels, local hints, disabled controls and client-owned validation. At E2E level, verify submission, server validation, mutation, navigation and refreshed output. See Blog #18 for the full forms and validation architecture.

Diagram 7: Form test layersRTL covers browser-owned behavior while Playwright covers the complete server round trip.
Form UIRTL
Server Action
Database/cache
Updated UIPlaywright

For useActionState, test a separated schema or rule directly, cover pending and error rendering where the component boundary is practical, then exercise the real action flow in a browser. Do not invent an action signature for the installed React version—inspect the project first. For useFormStatus, place the submit control within its parent form and test what the user sees while submission is pending.

Diagram 8: Pending-state contractA user receives immediate feedback, then a clear success or safe error state.
User submits
Pendingdisabled + loading label
Action completes
Success or error UI

Pending UI can be too brief to observe reliably in a fast local environment. Control the boundary at component level or use a purpose-built test service delay—not an arbitrary sleep in the assertion. The production interface should remain usable if JavaScript loads slowly or progressive enhancement is part of the design.

Testing Server Actions

A Server Action can parse FormData, validate input, authenticate the session, authorize the operation, call a service, mutate the database, invalidate cached data, redirect and translate expected failures into safe UI. A single giant mocked unit test is a poor model of that sequence. Split confidence by responsibility while keeping the production action readable.

Diagram 9: Server Action logic separationPure rules receive focused tests; framework orchestration stays covered by integration and browser flows.
Form → Server Action
Validate inputshared schema → unit tests
Authenticate + authorizepolicy tests
Mutationisolated integration
Redirect/revalidationE2E-visible result

A generic shape can look like this, but adapt it to the audited application rather than creating fake production code for an article:

'use server'
export async function createTodo(formData: FormData) {
  const parsed = todoSchema.safeParse({
    title: String(formData.get('title') ?? ''),
  })

  if (!parsed.success) return { error: 'Invalid title' }

  await requireTodoWritePermission()
  await saveTodo(parsed.data)
  revalidatePath('/todos')
}
A layered testing pyramid with many unit checks, an isolated service and database layer, and one complete browser form flow
Server Action confidence grows from focused rules through isolated persistence to a complete form-to-action-to-UI journey.

Extracting and Testing Validation Logic

Use the same validation schema in production and tests; never copy it into the test. If the real application uses Zod, Valibot, Yup or a custom validator, preserve that choice. This publishing repository contains none of those libraries, so the following Zod example is illustrative:

describe('todoSchema', () => {
  it('rejects an empty title', () => {
    const result = todoSchema.safeParse({ title: '' })
    expect(result.success).toBe(false)
  })

  it('accepts a meaningful title', () => {
    const result = todoSchema.safeParse({ title: 'Review the test plan' })
    expect(result.success).toBe(true)
  })
})

Cover boundaries and normalized values that matter to the domain. A client-side required attribute improves feedback but is not a security boundary. Repeat validation on the server because callers can invoke an endpoint without your UI.

Testing Authentication and Authorization

Authentication answers “Who are you?” Authorization answers “Are you allowed to do this?” A valid session does not imply permission to edit every record. Server Actions must enforce both on the server. Revisit Blog #13 for authentication architecture and Blog #27 for cross-site request protections; the dedicated authentication testing guide remains planned.

Diagram 10: Authentication vs authorizationA mutation proceeds only after identity and permission are both established.
Request
Authenticated?identity
Authorized?permission
Perform mutation

Unit-test pure policy helpers with owners, roles and resource attributes. Integration-test the real protected boundary where the environment supports it. For a sensitive action, keep an E2E allowed case and denied case. The denied test should confirm no mutation occurred and that the response reveals no sensitive record details.

Testing Database Mutations Safely

Never use the production database. Choose a disposable test database, transaction rollback, container, temporary schema or per-run namespace according to the actual architecture. Transaction rollback may be convenient for service integration tests, while an E2E flow spans multiple requests and often needs owned records plus explicit cleanup.

Diagram 11: Isolated test databaseTests may write only inside a controlled environment with known ownership and cleanup.
Tests
Test environment
Isolated test DB

Never: Tests → Production database

Seed the minimum deterministic state for each case. Use unique identifiers when tests run concurrently. Assert durable state when persistence is central: after submission, reload or revisit the page and verify the record still appears. Keep email, payments, storage and other side effects on vendor sandboxes or controlled adapters.

Testing redirect()

A mock can prove that one function was called with one string, but it does not prove that a user reached a usable destination. In a Playwright test, submit the form, assert toHaveURL(), then assert a distinctive accessible heading or message on the destination. Because redirect() interrupts control flow, keep it outside broad try/catch blocks that would swallow the redirect signal.

await page.getByRole('button', { name: /save/i }).click()
await expect(page).toHaveURL(/\/dashboard/)
await expect(page.getByRole('heading', { name: /dashboard/i }))
  .toBeVisible()

Testing revalidatePath() and revalidateTag()

Cache APIs are not merely function calls; their contract is fresh user-visible data. After a mutation, verify that the affected page or component shows the new state. When the application uses revalidateTag, current Next.js documentation requires a cache-life profile for stale-while-revalidate behavior; updateTag is the Server Action-oriented option for read-your-own-writes semantics. Inspect the installed Next.js version and existing cache strategy before choosing an assertion.

Diagram 12: Revalidation testThe meaningful result is fresh rendered data, not only an intercepted framework call.
Mutation
Revalidate path/tag
Fresh server render
Playwright assertion

For eventual stale-while-revalidate behavior, assert according to the product contract rather than requiring immediate replacement. For an action that must display its write immediately, choose the application API and cache design that provides that guarantee. Blog #9 covers the broader caching and revalidation model.

Error, Loading and Optimistic States

Cover expected validation errors, authorization failures and safe domain failures. Component tests can verify visible messages and accessible associations. E2E tests can confirm the complete page remains useful and does not expose stack traces, SQL messages or secrets. If error.tsx exists, exercise a controlled test-only failure or an expected application boundary rather than destabilizing production.

Test loading indicators only when they are a real user contract. Prefer deterministic controls over fixed sleeps. If the UI uses optimistic state, assert the immediate optimistic change, eventual server-confirmed state, and rollback or error state where supported.

Diagram 13: Optimistic mutation flowAn immediate update must eventually converge with the server or recover safely.
User action → Optimistic UI
Server successconfirm final state
Server errorrollback + safe message

Testing Server Action Forms and Server Data with Playwright

Start from user intent: navigate to the real page, fill labeled fields, submit through the visible control, and assert an outcome. Avoid calling action internals from the browser test. A critical mutation test should prove that validation, auth, persistence, redirect or refresh, and rendering agree.

test('creates a todo through the real form', async ({ page }) => {
  await page.goto('/todos')
  await page.getByLabel('Title').fill('Review test boundaries')
  await page.getByRole('button', { name: /add todo/i }).click()
  await expect(page.getByText('Review test boundaries')).toBeVisible()

  await page.reload()
  await expect(page.getByText('Review test boundaries')).toBeVisible()
})

Only use this route and wording if the audited application contains them. Use fixtures to create a unique record, authenticate through a safe test identity, and clean up through a controlled test API or database owner. Do not make the spec dependent on execution order.

Mocking Boundaries: What to Mock and What Not to Mock

Mock slow or unsafe boundaries in focused tests: a payment adapter, email sender, clock, random identifier or repository contract. Do not mock every collaborator. A test in which routing, request context, database, cache and rendering are all fake can pass while the product is broken.

Diagram 14: Mocking strategyMock narrow boundaries in focused tests and retain real integrations for the product contracts that matter.
Pure ruleno mock
Service boundaryfocused mock
Isolated integrationreal DB/service
Critical E2Ereal browser

Prefer dependency injection at a stable application boundary over patching deep Next.js internals. When a test must stub fetch, verify the meaningful request and response behavior, reset the stub after each case, and retain a contract or integration test for the remote shape.

Test Isolation and Test Data

Each test should own the data it creates. Generate a per-run identifier, create only required records and remove them even after failure. Do not share one mutable account across parallel workers. Freeze time only through an intentional seam and reset it after the case. Define environment guards so a destructive suite refuses to run when a production hostname, database or credential is detected.

Authentication storage state can contain live cookies and tokens. Keep it outside source control, scope it to a disposable account and expire it quickly. Browser traces and screenshots may capture personal data; limit collection and retention, and sanitize values before they reach CI artifacts.

Secure Server Action Pipeline

Server Actions are public server entry points in the security sense: never assume that only your React form can invoke them. Parse untrusted data, validate on the server, establish identity, check resource-level permission, perform the mutation, and return only safe expected errors. Apply rate limits or idempotency where the action risk calls for them.

Diagram 15: Secure Server Action pipelineEvery mutation crosses explicit validation and authorization gates before a controlled write.
Untrusted input
Parse + validate
Authenticate + authorize
Mutate
Safe result

Build an App Router Test Matrix from the Real Repository

Before creating tests, inventory code by execution boundary. Search the application for 'use client', 'use server', action=, useActionState, useFormStatus, redirect, revalidatePath, revalidateTag, cookies, headers, route handlers, validation schemas, database calls and authentication checks. The search result is not the test plan; it is the evidence used to build one.

Classify every high-risk target as pure logic, synchronous Server Component, async Server Component, Client Component, Server Action, Route Handler or integration layer. Then write down the observable contract, failure impact, environment dependency and cheapest reliable test. A pricing helper may need dozens of fast boundary cases. A read-only async page may need one browser assertion plus service-level coverage. A privileged delete action may justify policy unit tests, repository integration tests and two carefully isolated E2E journeys.

Audited targetPrimary evidenceImportant negative case
Pure utilityReturned value in VitestBoundary or malformed input
Validation schemaParsed normalized dataMissing, oversized or hostile input
Synchronous Server ComponentAccessible rendered outputEmpty or alternate props
Async Server ComponentBrowser-visible server dataEmpty, denied or safe error state
Client ComponentInteraction and visible UIKeyboard, invalid input or failed request
Server ActionMutation and final UIInvalid, unauthenticated and unauthorized requests
Repository/serviceIsolated integration resultConflict, missing record or dependency failure

Prioritize by risk instead of file count. Money, permissions, account ownership, destructive operations and compliance-sensitive data deserve deeper coverage than a decorative toggle. Record what is intentionally not covered and why. This makes the matrix a review tool rather than an automatic promise that every row needs every kind of test.

When a Direct Server Action Test Helps—and When It Does Not

Calling a Server Action function directly can be useful when its inputs and collaborators are controlled and the goal is to verify orchestration. For example, a focused test might supply FormData, stub one repository boundary, and assert the safe returned validation state. That still runs outside a real browser submission, request lifecycle and cache refresh. Label it accurately as a unit or focused integration test, not end-to-end coverage.

Direct tests become brittle when they reproduce framework internals. Mocking cookies(), headers(), navigation exceptions, cache stores and React form state in the same test creates a private version of Next.js that your application must maintain. If the action mostly coordinates those APIs, a Playwright flow often supplies clearer evidence with less custom simulation.

A balanced action suite asks distinct questions:

  • Does the schema accept and normalize valid input?
  • Does the permission rule deny the wrong owner or role?
  • Does the repository persist the intended shape in an isolated database?
  • Does the action return a safe expected error without leaking implementation details?
  • Can the real form submit successfully through a browser?
  • Does the final URL and rendered data reflect the mutation?

Do not assert every intermediate function call merely because a mock exposes it. Assertions should protect durable contracts. An implementation may replace revalidatePath with a tag strategy while preserving the same fresh UI; a user-oriented assertion should continue to pass.

+

Design Negative Cases Before the Happy Path Is Finished

Failures reveal whether boundaries are real. For validation, cover empty values, type coercion, length limits and domain-specific constraints. For authentication, cover the absence of a session. For authorization, authenticate a user who does not own the resource. For concurrency, consider a stale version or duplicate submission. For services, reproduce an expected timeout or domain failure through a controlled adapter.

Every denied mutation should prove two things: the user receives a safe response and protected state remains unchanged. A visible “not allowed” message alone is insufficient if the database write already occurred. Conversely, inspecting only the database misses whether the user is trapped on a broken page. Combine the cheapest state assertion with the appropriate visible assertion.

Server validation messages should not echo raw database errors, stack traces, tokens or rejected secrets. Associate field errors with their controls and expose summary messages through an appropriate live region. In Playwright, locate the error by role or associated text rather than a red CSS class. This protects accessibility and behavior at the same time.

Retries deserve special care. A user may double-click, a mobile connection may retry, or a browser may resubmit after navigation. High-impact actions can need idempotency or conflict handling. Test the business guarantee—one charge, one invitation or one state transition—rather than assuming the UI prevents every duplicate request.

Keep Component and Action Tests Maintainable

Place a test near its source when that matches repository convention, or use an established test directory. Do not reorganize the application solely to imitate a tutorial tree. Name cases by behavior: “denies a non-owner” communicates more than “calls authorize.” Keep fixture builders small and explicit so a reader can see which values matter.

Prefer factories that return valid defaults with deliberate overrides. A fixture containing every production column becomes hard to update and can hide the condition under test. Avoid global mutable fixtures. Create data per case or per worker, and make cleanup resilient when an assertion fails midway.

Semantic queries are part of the application contract. Use roles, labels and accessible names for controls. Use visible text when it is stable and meaningful. Reach for a test ID when no semantic query represents the element, and name it after behavior rather than layout. Avoid CSS paths, generated class names and repeated nth() selectors.

Keep each browser test focused on one meaningful journey, but do not split a single contract into so many specs that setup dominates. Use Playwright’s web-first assertions and automatic waiting. Wait for an observable state—URL, heading, response-backed content, enabled control—not a fixed number of milliseconds.

When a failure occurs, classify it as product regression, test defect, environment problem or external dependency outage. Preserve bounded traces and screenshots for investigation, remembering that artifacts can contain form values and cookies. A flaky test is information about synchronization, isolation or product instability; repeated retries are not a permanent fix.

Review the Boundary, Not Just the Test File

A test review should inspect the production boundary beside the spec. For a Client Component, confirm that server-only code does not leak into the client bundle and that serialized props contain no secret. For a Server Component, check whether the data query is bounded, authorization occurs before sensitive data is shaped, and empty states are deliberate. For a Server Action, review validation, resource-level authorization, mutation scope, error translation and cache behavior together.

Ask what a passing test actually proves. A mocked repository proves the action called a contract, not that the schema matches the database. A database integration test proves persistence, not that the form is labeled or the page refreshes. A browser test proves the configured journey, not every policy edge case. Write the missing evidence in the matrix instead of stretching one test beyond its meaning.

Also ask what can safely run in parallel. Pure tests normally can. Database tests need unique records, transactions or worker namespaces. Browser tests need independent accounts and URLs. If a suite requires serial execution, document the real shared resource and plan to remove it; serial ordering can hide state leaks and make CI much slower.

Finally, treat version changes as architecture changes. Read release notes and current official documentation before upgrading Next.js, React, Vitest, Testing Library or Playwright. Re-run the focused suite, production build and browser journeys. Pay special attention to cache semantics, Server Action behavior, React form hooks and test-environment support rather than assuming that an old workaround is still required.

Production Testing Strategy

Keep most checks close to the code and a smaller number close to user reality. Unit-test domain rules and security policies. Component-test interactive UI. Integration-test persistence and service contracts in disposable infrastructure. E2E-test the paths where a server-rendered page, browser interaction and server mutation must cooperate.

Do not run destructive tests against production. Production smoke checks should be read-only unless a rigorously isolated synthetic tenant and cleanup process exist. Monitor real errors and performance after deployment because pre-release tests cannot reproduce every network, browser and data condition.

Diagram 16: CI quality gateFast static and focused checks run before the production build and risk-based browser suite.
Install
Lint + types
Vitest
Build
Playwright
Deploy

Common Next.js App Router Testing Mistakes

  • Trying to unit-test every integrated behavior.
  • Treating async Server Components like Client Components.
  • Testing component internals instead of visible output.
  • Reading Client Component state instead of user behavior.
  • Putting validation, permission and persistence into one untestable action.
  • Mocking every dependency or deep Next.js internal.
  • Using a production database or production Server Action.
  • Testing revalidatePath only as a mocked call.
  • Testing redirect only as an implementation detail.
  • Trusting client-side validation or ignoring authorization.
  • Sharing mutable data or depending on test order.
  • Using fixed sleeps in E2E tests.
  • Skipping invalid, denied and service-failure cases.
  • Overusing snapshots for interactive behavior.
  • Restructuring the whole application only for testability.

Next.js 16 App Router Testing Best Practices

  • Match the test type to the execution boundary.
  • Unit-test pure validation and business logic.
  • Component-test Client Component behavior with semantic queries.
  • Use Playwright for async Server Component output and critical action flows.
  • Keep server-side authentication and authorization intact.
  • Test success, validation, denied and expected failure states.
  • Use isolated test services and owned data.
  • Avoid excessive framework mocks and arbitrary sleeps.
  • Test redirects from the user’s perspective.
  • Verify revalidated data is actually visible.
  • Keep cases independent and concurrency-safe.
  • Run lint, types, Vitest, build and a deliberate E2E suite in CI.
  • Review current Next.js and testing-tool documentation when versions change.

Testing & Quality Hub

#31 Vitest & React Testing LibraryPublished

#32 Playwright E2E TestingPublished

#33 Server Components, Client Components & Server Actions TestingPublished

#34 Route Handlers & API TestingPublished

#35 Authentication TestingPlanned

#36 Database TestingPlanned

#37 Mocking APIs & External ServicesPlanned

#38 Playwright Authentication & User FlowsPlanned

#39 Testing with GitHub ActionsPlanned

#40 Production Testing StrategyPlanned

Frequently Asked Questions

How do I test Server Components in Next.js?

Test synchronous, dependency-light Server Components with Vitest when practical. For async Server Components, test pure helpers separately and verify rendered behavior with Playwright.

Can Vitest test Server Components?

Vitest can cover synchronous Server Components and pure server-side logic. Compatibility still depends on the component dependencies and test environment.

Can Vitest test async Server Components?

Current Next.js guidance recommends end-to-end testing over unit testing for async Server Components because some tools do not fully support them.

How do I test Client Components?

Render them with React Testing Library, interact through user-event, and assert accessible, visible outcomes rather than internal state.

How do I test useState behavior?

Trigger the control a user would operate and assert the resulting UI. Do not read or mutate a component state variable directly.

How do I test Server Actions?

Unit-test extracted validation and business rules, integration-test safe service or repository boundaries, and use Playwright for the real form-to-action-to-UI flow.

Should I call Server Actions directly in unit tests?

Usually test their pure collaborators directly. A direct action test can be useful when the framework boundary is controlled, but it should not replace a browser-level workflow.

How do I test Server Action validation?

Export and test the same schema or pure validation helper used by the action, covering valid, invalid and boundary inputs without duplicating the rules.

How do I test Server Action redirects?

Submit through the browser, assert the final URL with Playwright, and verify meaningful destination content.

How do I test revalidatePath()?

Perform the safe mutation in an isolated environment and verify that the affected page displays fresh data. A mocked function call alone does not prove freshness.

How do I test revalidateTag()?

When the application uses tagged data, trigger the mutation and verify that a subsequent render exposes the updated user-visible result.

How do I test useActionState()?

Test separated validation and business rules, component-test feasible pending or error UI, and E2E-test the real action result.

How do I test useFormStatus()?

Submit the parent form and assert user-visible pending behavior such as a disabled button or loading label, then assert the completed state.

How do I test database mutations safely?

Use a disposable test database or namespace, deterministic seed data and cleanup. Never aim destructive tests at production.

Should I mock the database?

Mock a narrow repository boundary for focused unit tests, but retain integration coverage against an isolated database for important persistence behavior.

Should I mock Next.js APIs?

Only when a focused test needs a narrow boundary. Heavy mocking of redirects, request context and caching often creates brittle tests with weak confidence.

When should I use Playwright instead of Vitest?

Use Playwright for navigation, async Server Component output, real Server Action submissions, redirects, revalidation and critical browser workflows.

How do I test authorization in a Server Action?

Unit-test pure permission rules, integration-test the protected boundary where useful, and E2E-test critical allowed and denied journeys.

How do I avoid flaky App Router tests?

Isolate data, avoid execution-order dependencies, use semantic locators and web-first assertions, and never replace readiness checks with fixed sleeps.

What should run in CI?

A typical pipeline runs lint, types, Vitest, a production build and a risk-based Playwright suite using the project package manager and isolated services.

Current Official References

Next Steps

Audit the real application first, then create a small test matrix for its pure utilities, validation helpers, synchronous and async Server Components, Client Components, Server Actions, redirects, revalidation and authorization boundaries. Continue using Blog #31 for focused tests and Blog #32 for browser setup. For connected architecture, revisit Blog #7 on Server Actions, Blog #10 on Route Handlers, Blog #13 on authentication, Blog #18 on forms and Blog #27 on CSRF protection.

WhatsApp