Skip to main content

nextjs-testing

Sets up and writes tests for a Next.js App Router project, with Vitest and React Testing Library for components and server logic, and Playwright for end-to-end flows including async Server Components. Use when adding test tooling to a Next.js app, writing tests for a new page, form or Server Action, or deciding which layer a test belongs in.

Quellinformationen

Repository
autohandai/skillpack-nextjs-app-router
Letzte Quellaktivität
18. Oktober 2026 um 12:04
Erkannte Sprache von SKILL.md
Englisch
Sterne
0
Forks
0

Next.js Testing: choose the right test layer

Choose Vitest and React Testing Library for component and server logic tests, and Playwright for complete App Router flows including async Server Components.

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
nextjs-testing
description
Sets up and writes tests for a Next.js App Router project, with Vitest and React Testing Library for components and server logic, and Playwright for end-to-end flows including async Server Components. Use when adding test tooling to a Next.js app, writing tests for a new page, form or Server Action, or deciding which layer a test belongs in.
license
MIT
# Testing a Next.js App Router project ## Pick the layer first | What you are testing | Tool | Why | |---|---|---| | Pure functions, schemas, data helpers | Vitest | Fast, no DOM | | Client Components and synchronous Server Components | Vitest with jsdom and Testing Library | Renders in memory | | Server Action logic (validation, auth branches) | Vitest, with `next/*` modules mocked | The function is plain async code | | Async Server Components, routing, caching, redirects, full forms | Playwright | Vitest can't render async Server Components | If a test needs a real request, cookies or a redirect, write it in Playwright. Don't mock your way around the framework. ## Vitest setup ```bash npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths ``` ```ts // 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', exclude: ['e2e/**', 'node_modules/**'], }, }) ``` Add `"test": "vitest"` to `package.json` scripts. Drop `vite-tsconfig-paths` in a JavaScript project. ## Component test ```tsx // app/posts/new/post-form.test.tsx import { render, screen } from '@testing-library/react' import { expect, test, vi } from 'vitest' vi.mock('../actions', () => ({ createPost: vi.fn() })) import { PostForm } from './post-form' test('renders the title field and save button', () => { render(<PostForm />) expect(screen.getByRole('textbox', { name: /title/i })).toBeDefined() expect(screen.getByRole('button', { name: 'Save' })).toBeDefined() }) ``` Query by role and accessible name. If a query fails because a field has no label, fix the markup. ## Server Action test Mock what only exists inside a Next.js request. ```ts // app/posts/actions.test.ts import { beforeEach, expect, test, vi } from 'vitest' vi.mock('next/cache', () => ({ revalidatePath: vi.fn() })) vi.mock('next/navigation', () => ({ redirect: vi.fn(() => { throw new Error('NEXT_REDIRECT') }), })) vi.mock('@/lib/auth', () => ({ auth: vi.fn() })) vi.mock('@/lib/db', () => ({ db: { post: { create: vi.fn() } } })) import { createPost } from './actions' import { auth } from '@/lib/auth' import { db } from '@/lib/db' beforeEach(() => vi.clearAllMocks()) function form(fields: Record<string, string>) { const fd = new FormData() for (const [k, v] of Object.entries(fields)) fd.set(k, v) return fd } test('rejects an empty title without writing', async () => { vi.mocked(auth).mockResolvedValue({ user: { id: 'u1' } } as any) const state = await createPost({}, form({ title: '', body: 'x' })) expect(state.errors?.title).toBeDefined() expect(db.post.create).not.toHaveBeenCalled() }) test('refuses anonymous users', async () => { vi.mocked(auth).mockResolvedValue(null as any) const state = await createPost({}, form({ title: 'a', body: 'b' })) expect(state.message).toMatch(/sign in/i) expect(db.post.create).not.toHaveBeenCalled() }) ``` Every action should have one test per guard: unauthenticated, unauthorized, invalid input, and success. ## Playwright setup ```bash npm init playwright@latest ``` ```ts // playwright.config.ts import { defineConfig } from '@playwright/test' export default defineConfig({ testDir: './e2e', use: { baseURL: 'http://localhost:3000' }, webServer: { command: 'npm run build && npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 180_000, }, }) ``` Test against a production build. The dev server compiles on demand and hides caching behaviour. ```ts // e2e/posts.spec.ts import { test, expect } from '@playwright/test' test('a signed in user can publish a post', async ({ page }) => { await page.goto('/posts/new') await page.getByRole('textbox', { name: 'Title' }).fill('Hello') await page.getByRole('textbox', { name: 'Body' }).fill('First post') await page.getByRole('button', { name: 'Save' }).click() await expect(page).toHaveURL(/\/posts\/[\w-]+$/) await expect(page.getByRole('heading', { name: 'Hello' })).toBeVisible() }) ``` For signed in flows, log in once in a setup project and reuse `storageState`, rather than logging in for every test. ## Checks before you finish - `npx vitest run` and `npx playwright test` both pass locally. - Vitest doesn't pick up `e2e/` specs, and Playwright doesn't pick up `*.test.tsx`. - No test renders an async Server Component with Testing Library. - Each Server Action has tests for its auth and validation branches. - Playwright assertions use web-first `expect(locator)` matchers, not fixed waits.
Auf GitHub ansehen