Przejdź do treści

Jak testować Server Components w Next.js?

Izolacja logiki serwerowej bywa trudna. Zobacz, jak sprawnie zastępować połączenia z bazą danych i API w Vitest oraz kiedy odpuścić na rzecz testów E2E.

Maciej Sala

Founder StriveLab

3 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 4 czerwca 2026)

Problem: asynchroniczne Server Components w Next.js trudno testować jednostkowo

W praktyce oznacza to, że dla asynchronicznych oficjalna dokumentacja Next.js nadal rekomenduje zamiast prób pełnego renderowania ich w Vitest czy Jest. Testy jednostkowe oczywiście wciąż mają sens, ale trzeba je przesunąć poziom niżej, czyli do funkcji pomocniczych, funkcji pobierających dane, walidacji i synchronicznej warstwy widoku.

Trzy strategie testowania Server Components

Strategia 1: testuj logikę, nie cały Server Component

Zamiast renderować Server Component w teście, po prostu wyciągnij logikę (pobieranie danych, transformacje, walidację) do osobnych funkcji i testuj je w izolacji.

Code
// app/products/page.tsx — Server Component
import { getProducts, formatProductList } from '@/lib/products'
 
export default async function ProductsPage() {
  const products = await getProducts()
  const formatted = formatProductList(products)
 
  return (
    <main>
      <h1>Produkty ({formatted.total})</h1>
      <ul>
        {formatted.items.map((p) => (
          <li key={p.id}>
            {p.name} — {p.formattedPrice}
          </li>
        ))}
      </ul>
    </main>
  )
}
Code
// lib/products.ts — czysta logika, łatwa do testowania
export async function getProducts(filters?: ProductFilters) {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] },
  })
  return res.json()
}
 
export function formatProductList(products: Product[]) {
  return {
    total: products.length,
    items: products.map((p) => ({
      ...p,
      formattedPrice: new Intl.NumberFormat('pl-PL', {
        style: 'currency',
        currency: 'PLN',
      }).format(p.price),
      isOnSale: p.discount > 0,
    })),
  }
}
Code
// __tests__/lib/products.test.ts
import { describe, it, expect } from 'vitest'
import { formatProductList } from '@/lib/products'
 
describe('formatProductList', () => {
  it('should format prices in PLN', () => {
    const products = [
      { id: '1', name: 'Widget', price: 29.99, discount: 0 },
      { id: '2', name: 'Gadget', price: 149, discount: 10 },
    ]
 
    const result = formatProductList(products)
 
    expect(result.total).toBe(2)
    expect(result.items[0].formattedPrice).toContain('29,99')
    expect(result.items[0].isOnSale).toBe(false)
    expect(result.items[1].isOnSale).toBe(true)
  })
 
  it('should handle empty list', () => {
    const result = formatProductList([])
    expect(result.total).toBe(0)
    expect(result.items).toEqual([])
  })
})

To najskuteczniejszą strategią jest testowanie 80% logiki bez konieczności renderowania komponentu.

Strategia 2: rozdziel asynchroniczne pobieranie danych od synchronicznej warstwy widoku

Zamiast próbować renderować cały asynchroniczny Server Component, wydziel warstwę pobierania danych i osobny komponent prezentacyjny. Wtedy testujesz jednostkowo to, co jest stabilne: transformację danych i renderowanie widoku z właściwości.

Code
// app/products/page.tsx
import { getProducts, formatProductList } from '@/lib/products';
import { ProductsView } from './products-view';
 
export default async function ProductsPage() {
  const products = await getProducts();
  const formatted = formatProductList(products);
 
  return <ProductsView total={formatted.total} items={formatted.items} />;
}
Code
// app/products/products-view.tsx
export function ProductsView({
  total,
  items,
}: {
  total: number
  items: Array<{ id: string; name: string; formattedPrice: string }>
}) {
  return (
    <main>
      <h1>Produkty ({total})</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} — {item.formattedPrice}
          </li>
        ))}
      </ul>
    </main>
  )
}
Code
// __tests__/app/products/products-view.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ProductsView } from '@/app/products/products-view';
 
describe('ProductsView', () => {
  it('should render product list', () => {
    render(
      <ProductsView
        total={1}
        items={[
          { id: '1', name: 'Widget', formattedPrice: '29,99 zł' },
        ]}
      />
    );
 
    expect(screen.getByText('Produkty (1)')).toBeInTheDocument();
    expect(screen.getByText(/Widget/)).toBeInTheDocument();
  });
});

Taki podział daje dwa zyski: testy jednostkowe są stabilne, a asynchroniczną integrację ze środowiskiem uruchomieniowym Next.js pokrywasz E2E.

Strategia 3: mockowanie API Next.js w testach

Server Components często korzystają z cookies() i headers(). W Next.js 15+ to API jest asynchroniczne, więc podstawienia w testach też powinny zwracać Promise. params i searchParams najczęściej po prostu przekazujesz jako właściwości do testowanej funkcji pomocniczej lub widoku.

Code
// __tests__/helpers/next-mocks.ts
import { vi } from 'vitest'
 
// Podstaw cookies() i headers() jako asynchroniczne API
vi.mock('next/headers', () => ({
  cookies: vi.fn(async () => ({
    get: vi.fn((name: string) => {
      const cookieMap: Record<string, { value: string }> = {
        locale: { value: 'pl' },
        'auth-token': { value: 'mock-token' },
      }
      return cookieMap[name] || undefined
    }),
    getAll: vi.fn(() => []),
  })),
  headers: vi.fn(
    async () =>
      new Headers({
        'x-forwarded-for': '127.0.0.1',
        'user-agent': 'vitest',
      }),
  ),
}))
 
// Podstaw next/navigation
vi.mock('next/navigation', () => ({
  redirect: vi.fn((url: string) => {
    throw new Error(`REDIRECT:${url}`)
  }),
  notFound: vi.fn(() => {
    throw new Error('NOT_FOUND')
  }),
}))
 
// Podstaw next/cache
vi.mock('next/cache', () => ({
  revalidatePath: vi.fn(),
  revalidateTag: vi.fn(),
}))
Code
// Użycie w teście
import './helpers/next-mocks'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
 
it('should redirect unauthenticated users', async () => {
  vi.mocked(cookies).mockResolvedValue({
    get: vi.fn(() => undefined), // Brak tokena auth
    getAll: vi.fn(() => []),
  } as any)
 
  await expect(DashboardPage()).rejects.toThrow('REDIRECT:/login')
 
  expect(redirect).toHaveBeenCalledWith('/login')
})

Czego nie testować w Server Components

Nie testuj frameworka Next.js

Code
// ŹLE — testujesz, czy Next.js poprawnie generuje metadane
it('should have correct meta tags', () => { ... });
 
// ŹLE — testujesz, czy revalidatePath działa
it('should revalidate after update', () => { ... });
 
// ŹLE — testujesz, czy pamięć podręczna ISR działa poprawnie
it('should return cached data after 3600s', () => { ... });

To odpowiedzialność Next.js, nie Twojego kodu.

Nie testuj prostego renderowania bez logiki

Code
// Server Component, który tylko wyświetla dane
export default async function UserCard({ userId }: { userId: string }) {
  const user = await getUser(userId)
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

Jeśli komponent nie ma logiki (warunków, transformacji, przypadków brzegowych) — test nic nie wnosi. Testuj getUser() osobno.

Testuj Server Components, gdy mają logikę warunkową

Code
// Ten komponent testuj
export default async function PricingCard({
  productId,
}: {
  productId: string
}) {
  const product = await getProduct(productId)
  const user = await getCurrentUser()
 
  const discount = calculateDiscount(product, user)
  const finalPrice = product.price * (1 - discount)
  const showUpgradePrompt = !user.isPremium && discount === 0
 
  return (
    <div>
      <h2>{product.name}</h2>
      {discount > 0 && (
        <span className="text-green-600">-{discount * 100}%</span>
      )}
      <p>{formatPrice(finalPrice)}</p>
      {showUpgradePrompt && <UpgradeButton />}
    </div>
  )
}

Przetestuj funkcje takie jak calculateDiscount, logikę showUpgradePrompt czy formatowanie ceny. Zamiast renderować cały komponent, wydziel logikę biznesową i przetestuj ją bezpośrednio.

Konfiguracja Vitest dla testów Next.js

Code
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
 
export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./vitest.setup.ts'],
    include: ['**/*.test.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      include: ['lib/**', 'features/**', 'components/**'],
      exclude: ['**/*.test.*', '**/types.*'],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})
Code
// vitest.setup.ts
import '@testing-library/jest-dom/vitest'
Testy automatyczne komponentów i E2E w Cypress.
QA & Automation

Często zadawane pytania

Czy React Testing Library obsługuje Server Components?

Pośrednio i z ograniczeniami. Dla synchronicznych komponentów albo wydzielonej warstwy widoku renderowanie w JSDOM działa poprawnie. Dla asynchronicznych Server Components oficjalna dokumentacja Next.js nadal sugeruje E2E, bo Vitest i Jest nie odwzorowują pełnego środowiska uruchomieniowego App Routera — streamingu, granic Suspense ani zachowania frameworka wokół cookies() i headers().

Jaki procent pokrycia testami ma sens dla Server Components?

Celuj w 70–80% pokrycia logiki biznesowej, czyli funkcji w lib/, serwisów i funkcji pomocniczych. Objęcie komponentów w 100% mija się z celem, bo większość Server Components tylko wyświetla dane. Wartość testu rośnie tam, gdzie pojawia się warunek, transformacja danych lub przypadek brzegowy.

Co konkretnie wyciągnąć z komponentu, żeby dało się to testować?

Pobieranie danych, transformacje i walidację przenieś do osobnych funkcji w lib/, a renderowanie z właściwości do synchronicznego komponentu prezentacyjnego. Asynchroniczny Server Component zostaje wtedy cienkim spoiwem, które tylko woła funkcję pobierającą dane i przekazuje wynik do widoku — i to spoiwo pokrywasz E2E, a nie testem jednostkowym.

Jak podstawiać cookies() i headers() w testach Next.js 15 i nowszych?

Te API są asynchroniczne, więc podstawienie w teście musi zwracać Promise, dlatego używaj vi.fn(async () => ...) albo mockResolvedValue. Podstawienie cookies() zwraca obiekt z metodą get, a headers() instancję Headers. redirect() i notFound() najlepiej podstawić tak, by rzucały błąd, który łapiesz w asercji rejects.toThrow.

Vitest czy Jest do testowania Next.js?

Vitest, ponieważ działa natywnie z ESM, ma szybszy tryb obserwowania zmian i lepiej integruje się z narzędziami Vite, a API jest kompatybilne z Jest, więc migracja jest prosta. Dla nowych projektów Next.js nie ma dziś powodu zaczynać od Jest.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Jak poprawnie testować Server Actions w Next.js

Server Actions to publiczne endpointy POST, które każdy może wywołać z poziomu narzędzi deweloperskich, narzędzia curl lub skryptu, dlatego formularza w interfejsie nie wolno traktować jak granicy bezpieczeństwa. Jeśli akcja nie waliduje danych wejściowych i nie sprawdza uprawnień, staje się podatna na ataki. Testy pilnują, aby niezweryfikowane dane nie trafiły do bazy, niezalogowany użytkownik nie wykonał niedozwolonej modyfikacji, a rewalidacja oraz inne efekty uboczne uruchomiły się dopiero po pomyślnym zakończeniu operacji.

Maciej Sala

Maciej Sala

Founder StriveLab

Jak React Server Components wpływają na SEO i performance?

Przez lata React pchał, coraz więcej i więcej pracy do przeglądarki, w efekcie czego mamy większe bundle, hydratacja trwa dłużej, są puste loadingi i strony, które bez JavaScriptu straciły sens. App Router odwraca ten kierunek. React Server Components pozwalają renderować treść na serwerze bez wysyłania całej logiki do klienta, a Server Actions upraszczają formularze i mutacje. To ma znaczenie dla SEO, bo crawler szybciej dostaje HTML. Ma też znaczenie dla biznesu, bo użytkownik szybciej widzi i dostaje to, po co przyszedł.

Maciej Sala

Maciej Sala

Founder StriveLab

Cypress Component Testing w React i Next.js — kiedy naprawdę ma sens

Cypress Component Testing w React i Next.js bez marketingowej mgły. Kiedy daje przewagę nad RTL, jak go skonfigurować i gdzie kończą się jego możliwości.

Maciej Sala

Maciej Sala

Founder StriveLab