Vitest v Next.js 16: Testování Server Components, Server Actions a middleware (2026)

Praktický průvodce testováním Next.js 16 s Vitestem 3.2: konfigurace, testy Server Components, Server Actions, mockování next/navigation a proxy.ts, integrace do CI a kdy volit Playwright.

Aktualizováno: 28. srpna 2026

Vitest v Next.js 16 spustíte přes oficiální plugin @vitejs/plugin-react s vlastním vitest.config.ts, který mapuje aliasy z tsconfig.json a nastaví environment: 'jsdom'. Asynchronní React Server Components testujete voláním komponenty jako funkce (await Page()) a výsledek renderujete přes @testing-library/react. Server Actions jsou obyčejné asynchronní funkce, takže je voláte přímo v testu. Vitest 3.x běží 4–6× rychleji než Jest díky Vite dev serveru a nativnímu ESM. Právě proto ho na projektech s víc než 500 testy volím jako výchozí.

  • Vitest 3.2 (červen 2026) přinesl stabilní podporu asynchronních Server Components v @testing-library/react 17.
  • Next.js 16 už nevyžaduje next/jest; oficiální šablona v create-next-app --example with-vitest používá čistý Vite plugin.
  • Server Actions testujte jako běžné async funkce. Nepotřebujete HTTP klienta ani spuštěný Next.js server.
  • Pro middleware (proxy.ts) volejte handler přímo s ručně vytvořeným NextRequest; runtime je Edge-kompatibilní i pod Node.
  • Vitest je 4–6× rychlejší než Jest na středních projektech (200–1000 testů) a spouští se paralelně přes --pool=threads.
  • Playwright používejte pro E2E toky (login, checkout), Vitest pro komponenty, hooky a business logiku.

Proč zvolit Vitest místo Jest pro Next.js 16

Když jsem před dvěma lety migrovala stockholmský e-commerce projekt z Jestu na Vitest, celková doba běhu testů klesla z 87 vteřin na 19 vteřin, a to bez jakékoli změny testů samotných. Rozdíl není magie. Vitest sdílí transformaci s Vite dev serverem, takže testy běží nad stejným ESM grafem jako aplikace. Jest naopak transpiluje každý soubor přes Babel a musí simulovat ESM přes vlastní modul-resolver, což je pomalé i s swc.

Pro Next.js 16, který interně používá Turbopack a plně ESM moduly (viz migrace Turbopacku do produkce), je Vitest přirozenější volba. Nemusíte řešit dva paralelní transformační pipeline. Vitest také podporuje vi.mock() se stejnou syntaxí jako Jest, takže migrace stávajících testů zabere obvykle hodiny, ne dny. V mém případě šlo o find-and-replace jest.mockvi.mock a přidání importu import { describe, it, expect, vi } from 'vitest'.

Další praktická výhoda: Vitest má nativní podporu TypeScriptu bez konfigurace, watch mód reaguje pod 100 ms a UI dashboard (vitest --ui) mi ušetřil hodiny při ladění flaky testů. Jediné, co Jestu závidím, je bohatší ekosystém pluginů. Pro Next.js aplikace mi ale nikdy nic zásadního nechybělo.

Instalace a konfigurace Vitest v Next.js 16

Předpokládám čerstvý projekt vytvořený přes npx create-next-app@latest s App Routerem a TypeScriptem. Nainstalujte tyto balíčky:

pnpm add -D vitest@^3.2.0 @vitejs/plugin-react@^5 \
  @testing-library/react@^17 @testing-library/jest-dom@^6.5 \
  @testing-library/user-event@^14 jsdom@^25 \
  @vitest/coverage-v8@^3.2.0

Vytvořte vitest.config.ts v rootu:

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',
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    include: ['**/*.{test,spec}.{ts,tsx}'],
    exclude: ['**/node_modules/**', '**/e2e/**'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: ['**/*.config.*', '**/e2e/**', '.next/**'],
    },
  },
})

Setup soubor tests/setup.ts nastavuje globální matchery:

import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'

afterEach(() => {
  cleanup()
})

V package.json přidejte skripty:

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

Jak testovat React Server Components

Do verze Vitest 3.2 (červen 2026) bylo testování async Server Components utrpení. Museli jste je obalovat do Suspense a čekat na resolution. Aktuální React 19.2 a @testing-library/react 17 to řeší nativně přes await. Ukázka komponenty, která načítá data z databáze:

// app/products/product-list.tsx
import { db } from '@/lib/db'

export async function ProductList({ categoryId }: { categoryId: string }) {
  const products = await db.product.findMany({
    where: { categoryId },
    take: 10,
  })

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

A odpovídající test. Komponentu voláte jako funkci, dostanete JSX zpět a předáte ho render():

// app/products/product-list.test.tsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { ProductList } from './product-list'

vi.mock('@/lib/db', () => ({
  db: {
    product: {
      findMany: vi.fn().mockResolvedValue([
        { id: '1', name: 'Espresso' },
        { id: '2', name: 'Flat White' },
      ]),
    },
  },
}))

describe('ProductList', () => {
  it('vykreslí produkty pro danou kategorii', async () => {
    const ui = await ProductList({ categoryId: 'coffee' })
    render(ui)

    expect(screen.getByText('Espresso')).toBeInTheDocument()
    expect(screen.getByText('Flat White')).toBeInTheDocument()
  })
})

Klíčové je pochopit, že RSC nejsou stateful. Jsou to čisté funkce, které vrátí JSX. Testujete tedy vstup → výstup, nikoli lifecycle. Pro komponenty s use cache direktivou (viz průvodce Cache Components v Next.js 16) mockujte celou datovou vrstvu. Cache runtime v testech nepotřebujete, jen deterministická data.

Testování Server Actions s Vitestem

Server Actions jsou z pohledu Vitest jen asynchronní funkce s direktivou 'use server'. Vitest tuto direktivu ignoruje a spustí funkci v Node prostředí. Pro action, která validuje formulář přes Zod a zapisuje do DB, vypadá test takto:

// app/actions/create-product.ts
'use server'
import { z } from 'zod'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

const Schema = z.object({
  name: z.string().min(2),
  priceCents: z.number().int().positive(),
})

export async function createProduct(formData: FormData) {
  const parsed = Schema.safeParse({
    name: formData.get('name'),
    priceCents: Number(formData.get('priceCents')),
  })

  if (!parsed.success) {
    return { ok: false as const, errors: parsed.error.flatten() }
  }

  const product = await db.product.create({ data: parsed.data })
  revalidatePath('/products')
  return { ok: true as const, product }
}
// app/actions/create-product.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createProduct } from './create-product'

const create = vi.fn()
const revalidatePath = vi.fn()

vi.mock('@/lib/db', () => ({ db: { product: { create } } }))
vi.mock('next/cache', () => ({ revalidatePath }))

beforeEach(() => {
  create.mockReset()
  revalidatePath.mockReset()
})

function fd(fields: Record<string, string>) {
  const f = new FormData()
  for (const [k, v] of Object.entries(fields)) f.append(k, v)
  return f
}

describe('createProduct', () => {
  it('vrátí chybu při prázdném názvu', async () => {
    const res = await createProduct(fd({ name: '', priceCents: '500' }))
    expect(res.ok).toBe(false)
    expect(create).not.toHaveBeenCalled()
  })

  it('vytvoří produkt a revaliduje /products', async () => {
    create.mockResolvedValue({ id: '42', name: 'Cortado', priceCents: 500 })
    const res = await createProduct(fd({ name: 'Cortado', priceCents: '500' }))

    expect(res.ok).toBe(true)
    expect(create).toHaveBeenCalledWith({
      data: { name: 'Cortado', priceCents: 500 },
    })
    expect(revalidatePath).toHaveBeenCalledWith('/products')
  })
})

Detailní vzory pro validaci a useActionState najdete v samostatném článku o Server Actions v Next.js 16. Pro účely testování si zapamatujte tři pravidla: mockujte next/cache jako celek, konstruujte FormData ručně přes helper, a testujte i chybové větve. Zod schémata mají zákeřné hrany kolem undefined vs. prázdný řetězec (loni mi to sežralo skoro celý pátek).

Mockování next/navigation, cache a fetch

Client komponenty používající useRouter, useSearchParams nebo usePathname potřebují mock next/navigation. Doporučuji jeden centrální setup:

// tests/mocks/next-navigation.ts
import { vi } from 'vitest'

export const mockPush = vi.fn()
export const mockReplace = vi.fn()

vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: mockPush,
    replace: mockReplace,
    refresh: vi.fn(),
    back: vi.fn(),
    forward: vi.fn(),
  }),
  usePathname: () => '/products',
  useSearchParams: () => new URLSearchParams('sort=price'),
  redirect: vi.fn((url: string) => {
    throw new Error(`REDIRECT: ${url}`)
  }),
  notFound: vi.fn(() => {
    throw new Error('NOT_FOUND')
  }),
}))

Pro fetch používám Mock Service Worker místo vi.fn() na globální fetch. Důvod je jednoduchý: MSW zachytává požadavky na síťové úrovni, takže testujete i skutečnou konstrukci URL a headers. Nastavení pro Node runtime:

// tests/setup.ts (rozšířený)
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { afterAll, afterEach, beforeAll } from 'vitest'

export const server = setupServer(
  http.get('https://api.example.com/products', () =>
    HttpResponse.json([{ id: '1', name: 'Cortado' }])
  ),
)

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Testování middleware a proxy.ts

V Next.js 16 nahradil klasický middleware.ts soubor proxy.ts s explicitním exportem (podrobnosti v průvodci migrace na proxy.ts). Testování je díky tomu čistší. Voláte handler jako obyčejnou funkci s NextRequest:

// proxy.ts
import { NextRequest, NextResponse } from 'next/server'

export async function proxy(request: NextRequest) {
  const token = request.cookies.get('session')?.value

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

export const config = { matcher: ['/dashboard/:path*'] }
// proxy.test.ts
import { describe, it, expect } from 'vitest'
import { NextRequest } from 'next/server'
import { proxy } from './proxy'

function makeReq(pathname: string, cookies: Record<string, string> = {}) {
  const req = new NextRequest(new URL(pathname, 'https://example.com'))
  for (const [name, value] of Object.entries(cookies)) {
    req.cookies.set(name, value)
  }
  return req
}

describe('proxy', () => {
  it('přesměruje na /login když chybí session cookie', async () => {
    const res = await proxy(makeReq('/dashboard/settings'))
    expect(res.status).toBe(307)
    expect(res.headers.get('location')).toContain('/login')
  })

  it('pustí požadavek s platnou session', async () => {
    const res = await proxy(
      makeReq('/dashboard/settings', { session: 'abc123' })
    )
    expect(res.status).toBe(200)
  })
})

Runtime NextRequest funguje pod Node i Edge, takže tento test běží stejně v Vitest jako v produkci na Vercelu. U složitějších matcherů (rate limiting, geo-based routing) doporučuji parametrizované testy přes it.each. Pokrytí kombinatoriky je zásadní, jinak vám první produkční incident najde díru za vás.

Coverage, rychlost a integrace do CI

V-osmičkový provider (@vitest/coverage-v8) je 2–3× rychlejší než Istanbul a používá stejný nativní coverage jako Node.js debugger. Na jednom mém projektu s 1 400 testy klesl čas z 42 s (Istanbul) na 14 s (v8). Doporučená konfigurace pro CI:

// vitest.config.ts (rozšíření)
coverage: {
  provider: 'v8',
  reporter: ['text', 'lcov', 'json-summary'],
  thresholds: {
    lines: 80,
    functions: 75,
    branches: 70,
    statements: 80,
  },
  exclude: [
    '**/*.config.*',
    '**/e2e/**',
    '.next/**',
    'app/**/layout.tsx',
    'app/**/loading.tsx',
    'app/**/not-found.tsx',
  ],
},

GitHub Actions workflow, který používám na klientských projektech:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  vitest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v5
        with:
          node-version: '22'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v5
        with:
          files: ./coverage/lcov.info
          fail_ci_if_error: true

Pro paralelizaci nad monorepy používám --pool=threads --poolOptions.threads.maxThreads=4. Vlákna sdílejí importy modulů (na rozdíl od fork poolu), takže start je rychlejší. Pozor však na testy s globálním stavem. V takovém případě volte --pool=forks a přijměte 20–30 % overhead za izolaci.

Kdy sáhnout po Playwrightu místo Vitestu

Vitest je perfektní pro unit a integration testy, ale falešně pozitivní zelené světlo je snadné. Testujete komponentu izolovaně a přehlédnete regresi v layoutu, hydraci nebo streaming boundary. Pro tyto scénáře používám Playwright jako doplněk. Rozdělení, které se mi osvědčilo:

ScénářVitestPlaywright
Čistá funkce nebo hookAnoNe
Server Component s datyAno (mock DB)Ne
Server Action business logikaAnoNe
Formulář s validací klientaAnoDoplněk
End-to-end tok (login → nákup)NeAno
Hydratace client komponentOmezeněAno
Vizuální regrese (screenshoty)NeAno
Cross-browser (WebKit, Firefox)NeAno

Konkrétní pravidlo, kterým se řídím: pokud test potřebuje sledovat opravdový network waterfall, LCP nebo animaci, patří do Playwrightu. Když jsem loni ladila regresi TTFB u jednoho SaaS zákazníka, Vitest test procházel. Problém odhalil až Playwright s page.waitForLoadState('networkidle') a měřením performance.getEntriesByType('navigation'). Flame graf jsem si pak zobrazila přes page.tracing.start v Chromiu.

Častá úskalí a jak je řeším

Chyba „Cannot find module 'server-only'". Next.js chrání server moduly balíčkem server-only, který v testu selže. Přidejte alias do vitest.config.ts: resolve: { alias: { 'server-only': require.resolve('./tests/mocks/server-only.ts') } } a v mock souboru exportujte prázdný objekt.

Async Server Component vrací undefined. Zapomněli jste await. Server Components jsou async funkce; volání bez await vrátí Promise, ne JSX. TypeScript by měl varovat, ale jen s strict: true.

Testy padají po upgradu na Vitest 3.2. Projděte migrační průvodce Vitest. V 3.0 se změnil formát test.each a v 3.1 se zpřísnila kontrola onUnhandledRequest u MSW. Přesně na tohle jsem narazila při aktualizaci vlastního boilerplate a hodinu jsem hledala, proč MSW hlásí unhandled request na endpoint, který jsem zjevně mockovala (chyběl trailing slash).

Coverage nezobrazuje TSX soubory. V v8 provideru přidejte all: true do coverage a rozšiřte include na ['app/**/*.{ts,tsx}', 'lib/**/*.{ts,tsx}']. Bez all: true reportuje jen soubory, které byly během běhu importovány.

Test běží 30× déle, než by měl. Pravděpodobně načítáte skutečnou databázi nebo síť. Zkontrolujte, zda vi.mock() je na modulové úrovni (ne uvnitř beforeEach), Vitest hoisting funguje jen tak. Pokud používáte Prisma, nakonfigurujte oficiální Next.js Vitest setup s in-memory SQLite adaptérem.

Často kladené otázky

Podporuje Vitest React Server Components?

Ano, od Vitest 3.2 a React 19.2. Async Server Component voláte jako funkci (await Page(props)), výsledek předáte render() z @testing-library/react 17+. Starší verze vyžadovaly obalení do Suspense, což bylo křehké.

Je Vitest rychlejší než Jest pro Next.js projekty?

V mých měřeních 4–6× na projektech s 200–1 500 testy. Vitest sdílí Vite dev server a nativní ESM graf, Jest musí transpilovat přes Babel/SWC a simulovat ESM. Watch mód Vitestu reaguje pod 100 ms, Jest typicky 800–2 000 ms.

Jak mockovat useRouter z next/navigation ve Vitestu?

Použijte vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn(), replace: vi.fn(), refresh: vi.fn() }) })). Doporučuji vytvořit centralizovaný mock v tests/mocks/next-navigation.ts a importovat ho v setup souboru.

Musím kvůli Vitestu vypnout Turbopack?

Ne. Vitest a Turbopack běží nezávisle. Turbopack obsluhuje dev server, Vitest má vlastní Vite transformaci. Můžete mít oba spuštěné současně (next dev --turbo a vitest --ui) bez konfliktů.

Dá se Vitestem testovat middleware v Next.js 16?

Ano, a je to čistší než v dřívějších verzích. V Next.js 16 exportujete funkci proxy() z proxy.ts, kterou v testu voláte přímo s ručně vytvořeným NextRequest. Není potřeba spouštět Next.js server ani simulovat Edge runtime.

Kdy použít Vitest a kdy Playwright?

Vitest pro unit testy komponent, Server Actions, hooks a business logiku. Playwright pro end-to-end toky (login, checkout), vizuální regrese, cross-browser testy a měření skutečných výkonových metrik. Většina mých projektů drží poměr zhruba 80 % Vitest, 20 % Playwright.

O Autorovi Hannah Lindqvist

Hannah is a performance-focused frontend engineer with nine years building React applications, the last five exclusively on Next.js. She spent three years at Spotify on the Web Player team optimizing TTI on low-end devices in emerging markets, then two years at a Stockholm-based e-commerce platform where she rebuilt the storefront on App Router and edge runtime, hitting sub-200ms TTFB across European POPs. She currently freelances for DTC brands and SaaS companies that need a Core Web Vitals rescue, and occasionally contributes patches to next/font and the Vercel Speed Insights SDK. She has presented at React Advanced London and React Summit Amsterdam on streaming rendering patterns. Her tutorials are the ones with Lighthouse screenshots and flame graphs, because she doesn't trust benchmarks she can't reproduce. Strong preference for Playwright over Cypress and Vitest over Jest.