Testing with Vitest and Playwright

Advanced
14 min

Testing with Vitest and Playwright

A Next.js application mixes pure functions, interactive components, server-only data code and full request flows, and each layer needs a different kind of test. Vitest covers the fast, isolated tests; Playwright covers what only a real browser can verify. After this lesson you will be able to configure both tools in a Next.js project, decide which layer each test belongs to, and work around the one thing Vitest cannot do.

Choosing the Right Layer

| Layer | Tool | What it checks | |---|---|---| | Pure logic (formatters, validators, DAL helpers) | Vitest | Inputs and outputs, no DOM | | Client Components and synchronous Server Components | Vitest + Testing Library | Rendered output and interactions in jsdom | | Route Handlers and Server Actions | Vitest | Called directly as functions | | Pages, navigation, forms, auth flows | Playwright | The real app in a real browser |

Async Server Components are the exception: Vitest cannot render async components, so their behaviour is covered by end-to-end tests.

Setting Up Vitest

bash
npm install --save-dev vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths
typescript
// vitest.config.mts import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [tsconfigPaths(), react()], test: { environment: "jsdom" }, });

vite-tsconfig-paths makes the @/ alias work in tests. Add "test": "vitest" to package.json scripts, then write a component test:

tsx
// components/Counter.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; import { expect, test } from "vitest"; import { Counter } from "./Counter"; test("increments on click", () => { render(<Counter />); fireEvent.click(screen.getByRole("button", { name: "Increment" })); expect(screen.getByText("Count: 1")).toBeDefined(); });

Components that call useRouter or usePathname need those hooks mocked, since there is no App Router in jsdom: vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }), usePathname: () => "/" })).

Testing Handlers and Actions Directly

Route Handlers are plain functions that take a Request, so call them without a server:

typescript
// app/api/health/route.test.ts import { expect, test } from "vitest"; import { GET } from "./route"; test("reports ok", async () => { const response = await GET(); expect(response.status).toBe(200); expect(await response.json()).toEqual({ status: "ok" }); });

Server Actions can be tested the same way by passing a FormData instance, with the database module mocked via vi.mock("@/lib/db") or pointed at a test database.

End-to-End Tests With Playwright

bash
npm init playwright@latest

The installer creates playwright.config.ts and an e2e folder. Point it at the dev server so tests start the app automatically:

typescript
// playwright.config.ts import { defineConfig } from "@playwright/test"; export default defineConfig({ testDir: "./e2e", use: { baseURL: "http://localhost:3000" }, webServer: { command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, });

The tests in the sample at the top of this lesson use role-based locators (getByRole, getByPlaceholder), which mirror how users and assistive technology find elements and survive markup changes. Run npx playwright test locally and in CI; npx playwright test --ui opens an interactive runner for debugging.

Tips

  • Seed a dedicated test database before Playwright runs and reset it between test files.
  • Keep unit tests next to the code (Counter.test.tsx) and end-to-end tests in e2e/.
  • Run Vitest on every commit and Playwright on pull requests; the first is seconds, the second minutes.
Quick Quiz
Question 1 of 3

Which kind of component can Vitest not render?

Key Takeaways

  • Use Vitest with Testing Library for logic, Client Components and synchronous Server Components.
  • Async Server Components are tested end-to-end, not in jsdom.
  • Route Handlers and Server Actions can be called directly in unit tests with mocked dependencies.
  • Playwright's webServer starts the app; role-based locators keep tests resilient.
  • Run fast unit tests on every commit and browser tests in CI before merging.

Next lesson: Performance Optimization and Bundle Analysis — measure Core Web Vitals and shrink what you ship.

Testing with Vitest and Playwright - Next.js | CodeYourCraft | CodeYourCraft