Design wechseln

Next.js Unit-Tests in der Praxis: Vollständiger Jest + React Testing Library Konfigurationsguide

Easton editorial illustration: component assembly loom

Montag um zehn schrieb der Tech Lead in die Gruppe: „Ab dieser Woche Unit-Tests fürs Projekt – Next.js mit Jest.“

React-Tests kannte ich, aber App Router und Server Components? Keine Ahnung, wie man die testet. npm install jest, npm test – und der Bildschirm explodierte in Rot:

Error: Cannot use import statement outside a module
SyntaxError: Unexpected token 'export'
Cannot find module 'next/navigation'

Zwei Tage zwischen Konfigurationsdateien, Stack Overflow und GitHub Issues. Dutzende Konfigurationsversuche – als die Tests endlich grün liefen, wollte ich die Tastatur hochwerfen.

Die Next.js-Testumgebung ist komplexer als gedacht, aber nicht mystisch: wenige Kernkonfigurationen, typische Fallstricke vermeiden – in zehn Minuten läuft es. Dieser Artikel führt Sie durch Next.js 15 + Jest + React Testing Library, teilt unsere Stolpersteine und zeigt Tests für Client Components, Server Components, Hooks und API-Mocks.

Testumgebung einrichten (von null auf eins)

Zuerst die Umgebung zum Laufen bringen. Die Konfiguration wirkt trocken – jede Option wird erklärt, damit Copy-Paste nicht ins Leere geht.

Abhängigkeiten installieren

Im Terminal alles auf einmal installieren:

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node @types/jest

Kurz zu den Paketen:

  • jest: Test-Framework
  • jest-environment-jsdom: simuliert Browser-DOM (für React-Komponenten)
  • @testing-library/react: React-Komponenten-Tests
  • @testing-library/jest-dom: zusätzliche Assertions (z. B. toBeInTheDocument())
  • ts-node und @types/jest: TypeScript-Unterstützung (bei JS optional)

Nach der Installation noch nicht testen – die Konfiguration fehlt.

jest.config.ts erstellen

Die zentrale Konfigurationsdatei. Im Projektroot jest.config.ts anlegen (bei JS: .js):

import type { Config } from 'jest'
import nextJest from 'next/jest'

// Lädt automatisch die Next.js-Konfiguration
const createJestConfig = nextJest({
  dir: './', // Next.js-Projektroot
})

const config: Config = {
  coverageProvider: 'v8', // Code-Coverage-Tool
  testEnvironment: 'jsdom', // Browser-Umgebung simulieren
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], // Setup vor Tests
}

// createJestConfig umschließt die Konfiguration und übernimmt Next.js-Transformationen
export default createJestConfig(config)

Warum next/jest?

next/jest übernimmt automatisch:

  • .css, .module.css (Mock, sonst Fehler in Tests)
  • Bilder, Fonts und statische Assets
  • .env-Umgebungsvariablen
  • TypeScript- und JSX-Transformation
  • Ausschluss von node_modules und .next

Ohne das alles manuell konfigurieren – das tut weh.

jest.setup.ts erstellen

Im Root jest.setup.ts anlegen:

import '@testing-library/jest-dom'

Importiert Jest-DOM-Matcher für Assertions wie:

  • expect(element).toBeInTheDocument()
  • expect(element).toHaveClass('active')
  • expect(element).toBeVisible()

Ohne diese Datei kennt Jest die Methoden nicht.

Pfad-Aliase konfigurieren (bei @/)

Bei Pfad-Alias wie import Button from '@/components/Button' muss Jest die Auflösung kennen.

In tsconfig.json (oder jsconfig.json):

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/components/*": ["components/*"],
      "@/lib/*": ["lib/*"]
    }
  }
}

In jest.config.ts moduleNameMapper ergänzen:

const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/lib/(.*)$': '<rootDir>/lib/$1',
  },
}

Warum? Jest kennt @/ standardmäßig nicht – nur relative oder absolute Pfade. Sie sagen Jest: „Bei @/components/Button suche unter <rootDir>/components/Button.“

Test-Skripte hinzufügen

In package.json:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}
  • npm test: einmal alle Tests
  • npm test:watch: bei Dateiänderungen automatisch neu testen

Konfiguration prüfen

Einfachen Test anlegen: __tests__/example.test.ts:

describe('Example Test', () => {
  it('should pass', () => {
    expect(1 + 1).toBe(2)
  })
})

npm test ausführen. Grünes PASS und 1 passed – Konfiguration steht.

Bei Fehlern: Abschnitt „Häufige Probleme“ – 90 % der Lösungen dort.

Komponenten-Tests in der Praxis

Konfiguration steht – jetzt echte Tests. Komponenten-Tests sind das Herz von Next.js-Tests; Client und Server Components unterscheiden sich deutlich.

Client Components testen (Standardablauf)

Client Components mit 'use client' sind vertraut und direkt testbar.

Beispiel LoginForm.tsx:

'use client'

import { useState } from 'react'

export default function LoginForm() {
  const [email, setEmail] = useState('')
  const [error, setError] = useState('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!email.includes('@')) {
      setError('请输入有效的邮箱')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="邮箱"
      />
      {error && <span role="alert">{error}</span>}
      <button type="submit">登录</button>
    </form>
  )
}

Testdatei LoginForm.test.tsx:

import { render, screen, fireEvent } from '@testing-library/react'
import LoginForm from '@/components/LoginForm'

describe('LoginForm', () => {
  it('should render input and button', () => {
    render(<LoginForm />)

    const emailInput = screen.getByPlaceholderText('邮箱')
    expect(emailInput).toBeInTheDocument()

    const submitButton = screen.getByRole('button', { name: '登录' })
    expect(submitButton).toBeInTheDocument()
  })

  it('should show error for invalid email', () => {
    render(<LoginForm />)

    const emailInput = screen.getByPlaceholderText('邮箱')
    const submitButton = screen.getByRole('button', { name: '登录' })

    fireEvent.change(emailInput, { target: { value: 'invalid-email' } })
    fireEvent.click(submitButton)

    const errorMessage = screen.getByRole('alert')
    expect(errorMessage).toHaveTextContent('请输入有效的邮箱')
  })
})

Testablauf:

  1. render() rendert die Komponente
  2. screen.getByXxx() findet Elemente (Rolle, Text, Placeholder)
  3. fireEvent simuliert Nutzeraktionen
  4. expect() prüft Ergebnisse

Tipp: lieber getByRole als getByTestId – Rollen wie button oder alert entsprechen dem, was Nutzer sehen; Tests bleiben stabiler.

Server Components testen (etwas unbequem)

Server Components sind zentral in Next.js 15 – Jest unterstützt sie nur eingeschränkt.

Kernproblem: Jest unterstützt keine async Server Components.

Beispiel mit Datenbankabruf:

// app/posts/page.tsx (Server Component)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Direkter Test → Fehler: Objects are not valid as a React child.

Was tun?

Drei Ansätze:

Option 1: Geschäftslogik auslagern, Pure Functions testen

// lib/posts.ts
export async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

// lib/posts.test.ts
import { getPosts } from './posts'

describe('getPosts', () => {
  it('should fetch posts successfully', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve([{ id: 1, title: 'Test Post' }]),
      })
    ) as jest.Mock

    const posts = await getPosts()
    expect(posts).toHaveLength(1)
    expect(posts[0].title).toBe('Test Post')
  })
})

Sie testen den Datenabruf, nicht die Komponente. Komplexe async Logik separat – die Komponente rendert nur noch einfach.

Option 2: synchrone Server Components testen

Ohne async normal testbar:

// components/Title.tsx (Server Component, kein async)
export default function Title({ text }: { text: string }) {
  return <h1 className="title">{text}</h1>
}

// components/Title.test.tsx
import { render, screen } from '@testing-library/react'
import Title from './Title'

describe('Title', () => {
  it('should render title', () => {
    render(<Title text="Hello World" />)
    const heading = screen.getByRole('heading', { level: 1 })
    expect(heading).toHaveTextContent('Hello World')
  })
})

Option 3: E2E-Tests ergänzen

Für komplexe Server Components: Playwright oder Cypress. Jest für Frontend-Logik, E2E für den Gesamtflow.

Mein Ansatz: Kernlogik als Pure Functions, Server Components nur einfaches Rendering, E2E für den Rest.

Interaktions-Tests

@testing-library/react bietet viele Hilfsmittel:

import { render, screen, fireEvent, waitFor } from '@testing-library/react'

// Klick
fireEvent.click(button)

// Eingabe
fireEvent.change(input, { target: { value: 'test' } })

// auf async Update warten
await waitFor(() => {
  expect(screen.getByText('Success')).toBeInTheDocument()
})

// Sichtbarkeit
expect(element).toBeVisible()

// Klassen
expect(element).toHaveClass('active')

Vollständiges async Interaktionsbeispiel:

it('should submit form successfully', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ success: true }),
    })
  ) as jest.Mock

  render(<LoginForm />)

  const emailInput = screen.getByPlaceholderText('邮箱')
  const submitButton = screen.getByRole('button', { name: '登录' })

  fireEvent.change(emailInput, { target: { value: '[email protected]' } })
  fireEvent.click(submitButton)

  await waitFor(() => {
    expect(screen.getByText('登录成功')).toBeInTheDocument()
  })
})

waitFor wartet auf async Abschluss. Bei useEffect oder async State-Updates unverzichtbar – sonst läuft die Assertion vor dem Update und liefert Fehlalarme.

Hook-Test-Strategien

Custom Hooks sind React-Kern – wie testen? Manche testen im Komponentenkontext und vermischen Hook- und UI-Logik. Besser: renderHook.

Einfache Hooks testen

Beispiel Zähler-Hook:

// hooks/useCounter.ts
import { useState } from 'react'

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}

Test:

// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter())
    expect(result.current.count).toBe(0)
  })

  it('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10))
    expect(result.current.count).toBe(10)
  })

  it('should increment count', () => {
    const { result } = renderHook(() => useCounter())

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

  it('should reset count', () => {
    const { result } = renderHook(() => useCounter(5))

    act(() => {
      result.current.increment()
      result.current.increment()
    })

    expect(result.current.count).toBe(7)

    act(() => {
      result.current.reset()
    })

    expect(result.current.count).toBe(5)
  })
})

Wichtig:

  • renderHook rendert den Hook
  • act umschließt State-Updates (React-Regel: Update abwarten, dann asserten)
  • result.current liefert den Hook-Rückgabewert

Hooks mit Context testen

Bei Context-Abhängigkeit (z. B. Auth) brauchen Sie einen Provider.

// hooks/useAuth.ts
import { useContext } from 'react'
import { AuthContext } from '@/contexts/AuthContext'

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Mit Mock-Provider testen:

// hooks/useAuth.test.tsx
import { renderHook } from '@testing-library/react'
import { useAuth } from './useAuth'
import { AuthContext } from '@/contexts/AuthContext'

describe('useAuth', () => {
  it('should return auth context value', () => {
    const mockAuthValue = {
      user: { id: 1, name: 'Test User' },
      login: jest.fn(),
      logout: jest.fn(),
    }

    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <AuthContext.Provider value={mockAuthValue}>
        {children}
      </AuthContext.Provider>
    )

    const { result } = renderHook(() => useAuth(), { wrapper })

    expect(result.current.user).toEqual({ id: 1, name: 'Test User' })
    expect(result.current.login).toBeDefined()
  })

  it('should throw error when used outside provider', () => {
    const { result } = renderHook(() => useAuth())
    expect(result.error).toEqual(
      Error('useAuth must be used within AuthProvider')
    )
  })
})

Tipp: wrapper mit Provider – dann greift der Hook auf den Context zu.

Async Hooks testen (Datenabruf)

// hooks/useFetch.ts
import { useState, useEffect } from 'react'

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true)
        const response = await fetch(url)
        if (!response.ok) throw new Error('Network error')
        const json = await response.json()
        setData(json)
      } catch (err) {
        setError(err as Error)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [url])

  return { data, loading, error }
}

fetch mocken und auf State-Update warten:

// hooks/useFetch.test.ts
import { renderHook, waitFor } from '@testing-library/react'
import { useFetch } from './useFetch'

describe('useFetch', () => {
  beforeEach(() => {
    jest.resetAllMocks()
  })

  it('should fetch data successfully', async () => {
    const mockData = { id: 1, title: 'Test' }

    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockData),
      })
    ) as jest.Mock

    const { result } = renderHook(() => useFetch('/api/data'))

    expect(result.current.loading).toBe(true)
    expect(result.current.data).toBeNull()

    await waitFor(() => {
      expect(result.current.loading).toBe(false)
    })

    expect(result.current.data).toEqual(mockData)
    expect(result.current.error).toBeNull()
  })

  it('should handle fetch error', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: false,
      })
    ) as jest.Mock

    const { result } = renderHook(() => useFetch('/api/data'))

    await waitFor(() => {
      expect(result.current.loading).toBe(false)
    })

    expect(result.current.error).toBeTruthy()
    expect(result.current.error?.message).toBe('Network error')
    expect(result.current.data).toBeNull()
  })
})

Hinweise:

  • waitFor für async
  • beforeEach: Mocks zurücksetzen
  • Erfolg und Fehler testen

Hook-Tests: Abhängigkeiten mocken, State ändern, Ergebnis prüfen – drei Schritte für jeden Hook.

Mock-Techniken im Überblick

Mocks sind die Seele von Tests. Ohne Mock hängen Tests an echten APIs, DBs und Diensten – langsam und instabil. Next.js braucht spezielle Mocks – hier die wichtigsten.

Next.js-Routing mocken (am häufigsten)

Routing-Hooks (useRouter, usePathname, useSearchParams) sind in Tests standardmäßig nicht verfügbar.

useRouter mocken (App Router):

// __mocks__/next/navigation.ts
export const useRouter = jest.fn()
export const usePathname = jest.fn()
export const useSearchParams = jest.fn()

In der Testdatei:

import { useRouter } from 'next/navigation'

jest.mock('next/navigation', () => ({
  useRouter: jest.fn(),
  usePathname: jest.fn(),
  useSearchParams: jest.fn(),
}))

describe('NavigationComponent', () => {
  it('should navigate to home on button click', () => {
    const pushMock = jest.fn()
    ;(useRouter as jest.Mock).mockReturnValue({
      push: pushMock,
      back: jest.fn(),
      forward: jest.fn(),
    })

    render(<NavigationComponent />)

    const button = screen.getByRole('button', { name: '回到首页' })
    fireEvent.click(button)

    expect(pushMock).toHaveBeenCalledWith('/')
  })
})

usePathname mocken:

import { usePathname } from 'next/navigation'

jest.mock('next/navigation', () => ({
  usePathname: jest.fn(),
}))

describe('HeaderComponent', () => {
  it('should highlight active nav item', () => {
    ;(usePathname as jest.Mock).mockReturnValue('/about')

    render(<Header />)

    const aboutLink = screen.getByRole('link', { name: '关于' })
    expect(aboutLink).toHaveClass('active')
  })
})

Next.js Image mocken

next/image schlägt in Tests fehl – es braucht den Next.js-Bildoptimierungsdienst.

Option 1: als normales img mocken:

// __mocks__/next/image.tsx
const Image = ({ src, alt }: { src: string; alt: string }) => {
  return <img src={src} alt={alt} />
}

export default Image

Option 2: global in jest.config.ts:

const config: Config = {
  moduleNameMapper: {
    '^next/image$': '<rootDir>/__mocks__/next/image.tsx',
  },
}

Alle next/image-Imports werden automatisch ersetzt.

API-Requests mocken (drei Methoden)

Methode 1: global fetch (einfachste):

global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve({ data: 'mock data' }),
  })
) as jest.Mock

Methode 2: MSW (Mock Service Worker) (mächtiger):

npm install -D msw
// mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/posts', () => {
    return HttpResponse.json([
      { id: 1, title: 'Test Post' },
    ])
  }),

  http.post('/api/login', async ({ request }) => {
    const { email } = await request.json()
    if (email === '[email protected]') {
      return HttpResponse.json({ success: true })
    }
    return HttpResponse.json({ error: 'Invalid email' }, { status: 400 })
  }),
]
// mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

In jest.setup.ts:

import '@testing-library/jest-dom'
import { server } from './mocks/server'

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

MSW fängt alle Requests ab – kein global.fetch pro Test.

Methode 3: axios mocken:

npm install -D axios-mock-adapter
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'

const mock = new MockAdapter(axios)

describe('API Test', () => {
  afterEach(() => {
    mock.reset()
  })

  it('should fetch posts', async () => {
    mock.onGet('/api/posts').reply(200, [{ id: 1, title: 'Test' }])

    const response = await axios.get('/api/posts')
    expect(response.data).toHaveLength(1)
  })
})

Umgebungsvariablen mocken

Methode 1: process.env direkt setzen:

describe('Config Test', () => {
  const originalEnv = process.env

  beforeEach(() => {
    jest.resetModules()
    process.env = { ...originalEnv }
  })

  afterEach(() => {
    process.env = originalEnv
  })

  it('should use API URL from env', () => {
    process.env.NEXT_PUBLIC_API_URL = 'https://test-api.com'

    const { getApiUrl } = require('@/lib/config')
    expect(getApiUrl()).toBe('https://test-api.com')
  })
})

Methode 2: .env.test-Datei:

NEXT_PUBLIC_API_URL=https://test-api.com
DATABASE_URL=postgresql://test:test@localhost:5432/test

next/jest lädt sie automatisch.

Drittanbieter-Module mocken (Beispiel Prisma)

Bei Prisma wollen Sie in Tests keine echte DB.

// __mocks__/prisma.ts
export const prisma = {
  user: {
    findMany: jest.fn(),
    findUnique: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  },
  post: {
    findMany: jest.fn(),
    create: jest.fn(),
  },
}

In Tests:

import { prisma } from '@/lib/prisma'

jest.mock('@/lib/prisma', () => ({
  prisma: {
    user: {
      findUnique: jest.fn(),
    },
  },
}))

describe('getUserById', () => {
  it('should return user', async () => {
    const mockUser = { id: 1, name: 'Test User' }
    ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser)

    const result = await getUserById(1)
    expect(result).toEqual(mockUser)
    expect(prisma.user.findUnique).toHaveBeenCalledWith({
      where: { id: 1 },
    })
  })
})

Häufige Mock-Fehler

Problem 1: Cannot find module 'next/router'

Ursache: Next.js-Routing nicht gemockt.

Lösung: jest.mock('next/navigation') am Dateianfang.

Problem 2: Mock greift nicht

Ursache: jest.mock falsch platziert – muss oben stehen (nach imports).

import { useRouter } from 'next/navigation'

jest.mock('next/navigation')

describe('Test', () => {
  // ...
})

Problem 3: Umgebungsvariablen fehlen

Ursache: .env nicht geladen.

Lösung: jest.config.ts mit next/jest umschließen – lädt Umgebungsvariablen automatisch.

Mit diesen Mock-Techniken lassen sich die meisten Next.js-Besonderheiten testen.

Häufige Probleme und Fehlerbehebung

Beim Jest-Setup sind Fehler normal – 90 % sind wiederkehrend und haben Standardlösungen.

Fehler 1: Cannot use import statement outside a module

Meldung:

SyntaxError: Cannot use import statement outside a module

Ursache: Jest unterstützt ES Modules standardmäßig nicht; Code oder Dependencies nutzen import/export.

Lösung in jest.config.ts:

const config: Config = {
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  transformIgnorePatterns: [
    'node_modules/(?!(module-that-uses-esm)/)',
  ],
}

Bei bestimmten Paketen (nanoid, uuid) in die Ausnahmeliste:

transformIgnorePatterns: [
  'node_modules/(?!(nanoid|uuid)/)',
]

Fehler 2: Unexpected token ‘export’

Ursache: Datei wird nicht transformiert – ähnlich wie oben.

Lösung: transform und transformIgnorePatterns prüfen; Konfiguration mit next/jest umschließen:

import nextJest from 'next/jest'

const createJestConfig = nextJest({ dir: './' })

export default createJestConfig(config)

next/jest übernimmt TypeScript- und JSX-Transformation.

Fehler 3: Cannot find module ’@/components/…’

Ursache: Pfad-Alias @/ unbekannt.

Lösung: moduleNameMapper in jest.config.ts:

moduleNameMapper: {
  '^@/components/(.*)$': '<rootDir>/components/$1',
  '^@/lib/(.*)$': '<rootDir>/lib/$1',
  '^@/(.*)$': '<rootDir>/$1',
}

Pfade müssen mit tsconfig.json paths übereinstimmen.

Fehler 4: Objects are not valid as a React child

Ursache: async Server Component getestet – Jest unterstützt das nicht.

Lösung:

  1. async Logik in Pure Functions auslagern
  2. E2E mit Playwright oder Cypress

Nicht alles mit Jest erzwingen.

Fehler 5: act(…) warning

Meldung:

Warning: An update to Component inside a test was not wrapped in act(...).

Ursache: async State-Update (useEffect, setTimeout) – Test wartet nicht.

Lösung: waitFor oder act:

import { waitFor } from '@testing-library/react'

it('should update state', async () => {
  render(<Component />)

  await waitFor(() => {
    expect(screen.getByText('Updated')).toBeInTheDocument()
  })
})

Oder mit act:

import { act } from '@testing-library/react'

it('should trigger callback', async () => {
  await act(async () => {
    render(<Component />)
  })
})

Langsame Tests? Optimierung

  1. Parallel:
{
  "scripts": {
    "test": "jest --maxWorkers=4"
  }
}
  1. Nur geänderte Dateien:
npm test -- --onlyChanged
  1. Coverage deaktivieren (Debug):
{
  "scripts": {
    "test:fast": "jest --no-coverage"
  }
}
  1. test.skip für langsame Tests:
describe.skip('Slow Tests', () => {
  // wird übersprungen
})

Diagnose-Checkliste

Bei Fehlern der Reihe nach:

  1. jest.config.ts mit next/jest umschlossen?
  2. jest.setup.ts importiert @testing-library/jest-dom?
  3. ✅ Pfad-Aliase wie in tsconfig.json?
  4. ✅ Mocks für next/navigation, next/image?
  5. ✅ async mit waitFor oder act?
  6. ✅ kompatible Dependency-Versionen (besonders React 19 und Jest)?

Die Checkliste löst die meisten Probleme.

Fazit

Die Testumgebung einzurichten ist mühsam – läuft sie erst, hebt sie die Codequalität spürbar an.

Am Anfang wirkte Testen verschwendete Zeit – Feature in fünf Minuten, Tests zehn. Später: beim Refactoring und Bugfixing deutlich ruhiger. Tests grün – gut; rot – Sie wissen, was kaputtging.

Empfehlungen:

  • Nicht warten, bis das Projekt riesig ist – ab jetzt Tests zu neuen Features.
  • Keine 100-%-Coverage erzwingen – Kernlogik und fehleranfällige Stellen reichen.
  • Server Components nicht mit Jest erzwingen – Logik auslagern oder E2E.
  • Bei Fehlern die Checkliste durchgehen – meist schnell gelöst.

Konfigurationsdateien aus diesem Artikel speichern – beim nächsten Projekt kopieren, in zehn Minuten läuft die Testumgebung. Weniger Bugs, bessere Qualität.

Fragen bei Konfiguration oder Tests? Gerne in den Kommentaren – ich bin durch dieselben Fallen gelaufen und helfe, wo ich kann.

Next.js Jest Testumgebung komplett einrichten

Schritt-für-Schritt-Anleitung zur Einrichtung von Next.js 15 + Jest + React Testing Library

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Test-Abhängigkeiten installieren

    Jest und React Testing Library installieren:

    • npm install -D jest jest-environment-jsdom
    • npm install -D @testing-library/react @testing-library/dom @testing-library/jest-dom
    • npm install -D ts-node @types/jest (für TypeScript-Projekte)

    Paketübersicht:
    • jest: Test-Framework-Kern
    • jest-environment-jsdom: simuliert Browser-DOM-Umgebung
    • @testing-library/react: React-Komponenten-Tests
    • @testing-library/jest-dom: erweiterte Assertions (z. B. toBeInTheDocument)

    Nach der Installation Tests noch nicht ausführen – zuerst Konfigurationsdateien anlegen.
  2. 2

    Step 2: Jest-Konfigurationsdatei erstellen

    jest.config.ts im Projektroot anlegen und next/jest für Next.js-Features nutzen:

    ```typescript
    import type { Config } from 'jest'
    import nextJest from 'next/jest'

    const createJestConfig = nextJest({ dir: './' })

    const config: Config = {
    coverageProvider: 'v8',
    testEnvironment: 'jsdom',
    setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
    // Bei Pfad-Alias moduleNameMapper ergänzen
    moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/lib/(.*)$': '<rootDir>/lib/$1',
    },
    }

    export default createJestConfig(config)
    ```

    next/jest übernimmt automatisch: CSS/Bild-Mocks, Umgebungsvariablen, TypeScript-Transformation, node_modules-Ausschluss.
  3. 3

    Step 3: Jest-Setup-Datei erstellen

    jest.setup.ts im Projektroot anlegen und Jest-DOM-Erweiterungen importieren:

    ```typescript
    import '@testing-library/jest-dom'
    ```

    Damit stehen erweiterte Assertions zur Verfügung:
    • expect(element).toBeInTheDocument()
    • expect(element).toHaveClass('active')
    • expect(element).toBeVisible()
    • expect(element).toHaveTextContent('text')

    Ohne diesen Import führen die Methoden zu „not a function“-Fehlern.
  4. 4

    Step 4: Test-Skripte konfigurieren

    Test-Befehle in package.json ergänzen:

    ```json
    {
    "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
    }
    }
    ```

    Drei Befehle:
    • npm test: alle Tests (CI/CD)
    • npm run test:watch: Watch-Modus bei Dateiänderungen (Entwicklung)
    • npm run test:coverage: Code-Coverage-Bericht
  5. 5

    Step 5: Beispieltest zur Konfigurationsprüfung

    __tests__/example.test.ts anlegen und Umgebung prüfen:

    ```typescript
    describe('Example Test', () => {
    it('should pass basic assertion', () => {
    expect(1 + 1).toBe(2)
    })
    })
    ```

    npm test ausführen – grünes PASS bedeutet Erfolg.

    Bei Fehlern der Reihe nach prüfen:
    1. jest.config.ts mit createJestConfig umschlossen?
    2. jest.setup.ts korrekt importiert?
    3. package.json-Skripte korrekt?
    4. Pfad-Alias wie in tsconfig.json?

FAQ

Warum muss die Konfiguration mit next/jest umschlossen werden?
next/jest ist das offizielle Jest-Konfigurationstool von Next.js und übernimmt viele komplexe Einstellungen:

• automatisches Mocken von CSS und Bildern
• automatisches Laden von .env-Umgebungsvariablen
• automatische Transformation von TypeScript und JSX
• automatischer Ausschluss von node_modules und .next
• Next.js-Compiler-Transformationsregeln

Ohne next/jest müssten Sie all das manuell konfigurieren – fehleranfällig und aufwendig. Offiziell wird createJestConfig empfohlen.
Kann man Server Components mit Jest testen?
Teilweise, mit Einschränkungen:

• synchrone Server Components: normal testbar
• async Server Components: Jest unterstützt sie nicht – Fehler „Objects are not valid as a React child“

Empfohlen:
1. async Logik in Pure Functions auslagern und Datenabruf separat testen
2. Server Components nur einfach rendern, ohne komplexe Logik
3. Playwright oder Cypress für E2E über den gesamten Flow

Nicht alles mit Jest erzwingen – das richtige Werkzeug wählen.
Wie mocke ich useRouter in Tests?
App-Router-Hooks müssen gemockt werden:

```typescript
import { useRouter } from 'next/navigation'

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
useSearchParams: jest.fn(),
}))

const pushMock = jest.fn()
;(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
back: jest.fn(),
forward: jest.fn(),
})
```

jest.mock muss oben in der Testdatei stehen (nach imports), nicht in describe oder it.
Warum tritt der Fehler ‚Cannot use import statement outside a module‘ auf?
Jest unterstützt ES Modules standardmäßig nicht. Zwei Lösungen:

Option 1: transformIgnorePatterns (empfohlen)
```typescript
const config: Config = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [
'node_modules/(?!(nanoid|uuid)/)',
],
}
```

Option 2: next/jest korrekt umschließen
```typescript
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
export default createJestConfig(config)
```

In 90 % der Fälle nutzt ein npm-Paket ESM – Paketname in transformIgnorePatterns-Ausnahmeliste.
Wie mocke ich API-Requests? Welche Methode ist empfohlen?
Drei gängige Methoden, steigende Komplexität:

Methode 1: global fetch mocken (einfach, kleine Projekte)
```typescript
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
})
) as jest.Mock
```

Methode 2: MSW (Mock Service Worker, empfohlen für mittlere/große Projekte)
• alle Netzwerk-Requests abfangen
• komplexe Request/Response-Logik
• kein Mock pro Test nötig
• Installation: npm install -D msw

Methode 3: axios-mock-adapter (bei axios)
• speziell für axios
• einfache API

MSW empfiehlt sich wegen realistischerer Requests und Wiederverwendbarkeit im Team.
Tests laufen langsam – wie optimieren?
Vier praktische Methoden:

1. Parallel (am effektivsten)
```json
{ "scripts": { "test": "jest --maxWorkers=4" } }
```

2. Nur geänderte Dateien
```bash
npm test -- --onlyChanged
```

3. Coverage deaktivieren (beim Debuggen)
```json
{ "scripts": { "test:fast": "jest --no-coverage" } }
```

4. langsame Tests überspringen
```typescript
describe.skip('Slow E2E Tests', () => {
// wird übersprungen
})
```

Entwicklung: --onlyChanged und --no-coverage; CI/CD: vollständiger Lauf.
Pfad-Alias @/ konfiguriert, Tests schlagen trotzdem fehl?
jest.config.ts und tsconfig.json müssen übereinstimmen:

tsconfig.json (oder jsconfig.json):
```json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
```

jest.config.ts:
```typescript
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
}
```

Wichtig:
1. tsconfig: relative Pfade (ohne <rootDir>)
2. jest.config: absolute Pfade (mit <rootDir>)
3. Wildcard-Syntax konsistent (.*)

Nach Änderung Tests neu starten.

13 Min. Lesezeit · Veröffentlicht am: 7. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog