React Testing 2026: Vitest, React Testing Library und Best Practices

Professionelle Strategien für React-Tests im Jahr 2026 mit Vitest als Test-Runner und React Testing Library für verhaltensbasierte Komponententests.

React Testing 2026: Vitest, React Testing Library und Best Practices

React-Tests im Jahr 2026 basieren auf Vitest als dominierendem Test-Runner und React Testing Library (RTL) als Standard für Komponententests. Diese Kombination liefert schnelle Feedback-Schleifen, native ES-Modul-Unterstützung und Tests, die Verhalten statt Implementierungsdetails verifizieren.

Testing-Philosophie

Tests sollten so geschrieben werden, dass sie die Interaktion von Benutzern mit Komponenten nachbilden. Abfragen nach zugänglichen Rollen, Labels und Text sind sinnvoller als CSS-Klassen oder data-testid-Attribute. Dieser Ansatz erkennt echte Fehler und überlebt Refactorings.

Vitest-Konfiguration für React-Projekte

Vitest hat Jest als bevorzugten Test-Runner für moderne React-Anwendungen abgelöst. Native ES-Modul-Unterstützung eliminiert Transformations-Overhead, und die enge Vite-Integration bedeutet, dass die Konfiguration die Entwicklungsumgebung widerspiegelt.

Eine produktionsreife Vitest-Konfiguration verarbeitet JSX-Transformation, Pfad-Aliase und Test-Umgebungseinrichtung ohne Boilerplate:

vitest.config.tstypescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    // Use jsdom for DOM APIs and React rendering
    environment: 'jsdom',
    // Run setup file before each test file
    setupFiles: ['./src/test/setup.ts'],
    // Include only test files, exclude e2e
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    // Enable global test APIs (describe, it, expect)
    globals: true,
    // Generate coverage reports
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: ['node_modules', 'src/test/**'],
    },
  },
})

Die Setup-Datei erweitert Matcher und konfiguriert die Bereinigung zwischen Tests:

src/test/setup.tstypescript
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'

// Clean up rendered components after each test
afterEach(() => {
  cleanup()
})

// Mock window.matchMedia for responsive components
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation((query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
})

Dieses Setup bietet einen sauberen Ausgangspunkt für jeden Test und verarbeitet gängige Browser-API-Mocks, die jsdom nicht bietet.

Komponententests mit React Testing Library

React Testing Library erzwingt das Testen von Komponenten aus der Perspektive des Benutzers. Anstatt internen Zustand oder Prop-Werte zu inspizieren, interagieren Tests mit der gerenderten Ausgabe durch Accessibility-Abfragen.

Eine Suchkomponente filtert Ergebnisse und zeigt Ladezustände an:

SearchResults.tsxtsx
import { useState } from 'react'

interface SearchResultsProps {
  onSearch: (query: string) => Promise<string[]>
}

export function SearchResults({ onSearch }: SearchResultsProps) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<string[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function handleSearch() {
    if (!query.trim()) return
    setIsLoading(true)
    setError(null)
    try {
      const data = await onSearch(query)
      setResults(data)
    } catch (e) {
      setError('Search failed. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div>
      <label htmlFor="search-input">Search</label>
      <input
        id="search-input"
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button onClick={handleSearch} disabled={isLoading}>
        {isLoading ? 'Searching...' : 'Search'}
      </button>
      {error && <p role="alert">{error}</p>}
      <ul aria-label="Search results">
        {results.map((result, i) => (
          <li key={i}>{result}</li>
        ))}
      </ul>
    </div>
  )
}

Tests verifizieren das Verhalten der Komponente über Benutzerinteraktionen und asynchrone Zustände hinweg:

SearchResults.test.tsxtypescript
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { SearchResults } from './SearchResults'

describe('SearchResults', () => {
  it('displays results after successful search', async () => {
    // Arrange: mock the search function
    const mockSearch = vi.fn().mockResolvedValue(['React', 'Vue', 'Angular'])
    const user = userEvent.setup()
    render(<SearchResults onSearch={mockSearch} />)

    // Act: type a query and click search
    await user.type(screen.getByLabelText('Search'), 'framework')
    await user.click(screen.getByRole('button', { name: 'Search' }))

    // Assert: results appear in the list
    expect(await screen.findByText('React')).toBeInTheDocument()
    expect(screen.getByText('Vue')).toBeInTheDocument()
    expect(mockSearch).toHaveBeenCalledWith('framework')
  })

  it('shows loading state during search', async () => {
    // Create a promise that doesn't resolve immediately
    let resolveSearch: (value: string[]) => void
    const searchPromise = new Promise<string[]>((resolve) => {
      resolveSearch = resolve
    })
    const mockSearch = vi.fn().mockReturnValue(searchPromise)
    const user = userEvent.setup()
    render(<SearchResults onSearch={mockSearch} />)

    await user.type(screen.getByLabelText('Search'), 'test')
    await user.click(screen.getByRole('button', { name: 'Search' }))

    // Button shows loading state
    expect(screen.getByRole('button', { name: 'Searching...' })).toBeDisabled()

    // Resolve the search to clean up
    resolveSearch!([])
  })

  it('displays error message on search failure', async () => {
    const mockSearch = vi.fn().mockRejectedValue(new Error('Network error'))
    const user = userEvent.setup()
    render(<SearchResults onSearch={mockSearch} />)

    await user.type(screen.getByLabelText('Search'), 'query')
    await user.click(screen.getByRole('button', { name: 'Search' }))

    // Error message appears with alert role for screen readers
    expect(await screen.findByRole('alert')).toHaveTextContent(
      'Search failed. Please try again.'
    )
  })
})

Die userEvent-Bibliothek simuliert realistische Benutzerinteraktionen einschließlich Tastaturereignisse und Fokus-Management und erkennt Fehler, die synthetische Ereignisse übersehen.

Asynchrone Testmuster und waitFor

Asynchrone Operationen erfordern explizite Wartestrategien. React Testing Library bietet findBy-Abfragen, die getBy mit automatischem Warten kombinieren, sowie waitFor für komplexe Assertions.

Abfrage-Priorität

findBy ist waitFor + getBy vorzuziehen, wenn auf das Erscheinen von Elementen gewartet wird. waitFor sollte für Assertions auf bestehende Elemente reserviert werden, die ihren Zustand ändern.

Gängige asynchrone Muster erscheinen beim Testen von Datenabruf-Hooks und Komponenten:

async-patterns.test.tsxtypescript
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'

// Wait for element to appear (preferred for new elements)
it('loads user profile on mount', async () => {
  render(<UserProfile userId="123" />)
  
  // findBy returns a promise, automatically waits up to 1000ms
  const heading = await screen.findByRole('heading', { name: /john doe/i })
  expect(heading).toBeInTheDocument()
})

// Wait for element to disappear
it('removes loading spinner after data loads', async () => {
  render(<DataTable />)
  
  // Wait for spinner to be removed from DOM
  await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'))
  expect(screen.getByRole('table')).toBeInTheDocument()
})

// Wait for multiple conditions
it('enables submit button when form is valid', async () => {
  const user = userEvent.setup()
  render(<RegistrationForm />)

  const submitButton = screen.getByRole('button', { name: 'Submit' })
  expect(submitButton).toBeDisabled()

  await user.type(screen.getByLabelText('Email'), 'user@example.com')
  await user.type(screen.getByLabelText('Password'), 'SecurePass123!')

  // waitFor retries the assertion until it passes or times out
  await waitFor(() => {
    expect(submitButton).toBeEnabled()
  })
})

// Avoid false positives with queryBy for absence checks
it('does not show premium badge for free users', async () => {
  render(<UserCard subscription="free" />)
  
  // queryBy returns null instead of throwing, use for absence checks
  expect(screen.queryByText('Premium')).not.toBeInTheDocument()
})

Timeout-Anpassungen helfen bei langsameren Operationen, ohne die gesamte Testsuite zu verlangsamen:

typescript
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })

await waitFor(
  () => expect(mockSubmit).toHaveBeenCalled(),
  { timeout: 3000, interval: 100 }
)

Bereit für deine React / Next.js-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Mocking-Strategien: Module, APIs und Hooks

Vitests Mocking-Fähigkeiten behandeln externe Abhängigkeiten, ohne den Modulgraphen zu verschmutzen. Strategisches Mocking isoliert Komponenten von Netzwerkaufrufen, Drittanbieter-Bibliotheken und komplexen Abhängigkeiten.

Modul-Mocking ersetzt Imports auf Bundler-Ebene:

api.test.tstypescript
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUserData } from './api'

// Mock the entire fetch module
vi.mock('./http-client', () => ({
  httpClient: {
    get: vi.fn(),
  },
}))

import { httpClient } from './http-client'

describe('fetchUserData', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('transforms API response into user model', async () => {
    // Type-safe mock implementation
    vi.mocked(httpClient.get).mockResolvedValue({
      data: { id: 1, first_name: 'John', last_name: 'Doe' },
    })

    const user = await fetchUserData(1)

    expect(user).toEqual({
      id: 1,
      fullName: 'John Doe',
    })
  })
})

Für React-Hooks, die von Kontext oder externem Zustand abhängen, ermöglicht das renderHook-Utility isolierte Tests:

useAuth.test.tstypescript
import { renderHook, waitFor } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { useAuth } from './useAuth'
import { AuthProvider } from './AuthContext'

describe('useAuth', () => {
  it('returns authenticated user after login', async () => {
    const mockLogin = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
    
    const { result } = renderHook(() => useAuth(), {
      wrapper: ({ children }) => (
        <AuthProvider loginFn={mockLogin}>{children}</AuthProvider>
      ),
    })

    expect(result.current.user).toBeNull()
    expect(result.current.isAuthenticated).toBe(false)

    await result.current.login('alice@example.com', 'password')

    await waitFor(() => {
      expect(result.current.user).toEqual({ id: 1, name: 'Alice' })
      expect(result.current.isAuthenticated).toBe(true)
    })
  })
})

MSW (Mock Service Worker) bietet API-Mocking auf Netzwerkebene und ermöglicht Tests, die tatsächliche Fetch-Aufrufe ausführen:

handlers.tstypescript
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Test User',
    })
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json()
    if (body.email === 'valid@example.com') {
      return HttpResponse.json({ token: 'abc123' })
    }
    return HttpResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    )
  }),
]

Testen von React 19 Server Components

Server Components stellen einzigartige Testherausforderungen dar, da sie auf dem Server ausgeführt werden und HTML an den Client streamen. Direktes Unit-Testing erfordert einen anderen Ansatz als Client-Komponententests.

Für Server Components, die Datenabruf durchführen, testet man die Rendering-Ausgabe mit gemockten Datenquellen:

ServerComponent.test.tsxtypescript
import { describe, it, expect, vi } from 'vitest'

// Mock the data fetching function
vi.mock('./db', () => ({
  getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Server User' }),
}))

import { render, screen } from '@testing-library/react'
import { UserProfile } from './UserProfile.server'

describe('UserProfile Server Component', () => {
  it('renders user data from database', async () => {
    // Server components are async, await the component
    const Component = await UserProfile({ userId: '1' })
    render(Component)

    expect(screen.getByRole('heading')).toHaveTextContent('Server User')
  })
})

Integrationstests mit Playwright bieten die zuverlässigste Abdeckung für Server Components, indem sie die vollständige Rendering-Pipeline testen:

e2e/server-component.spec.tstypescript
import { test, expect } from '@playwright/test'

test('server component renders with streamed data', async ({ page }) => {
  await page.goto('/users/1')
  
  // Wait for streaming to complete
  await expect(page.getByRole('heading', { name: /profile/i })).toBeVisible()
  await expect(page.getByText('Server User')).toBeVisible()
})

Testen von Custom Hooks mit komplexem Zustand

Custom Hooks, die komplexen Zustand, Seiteneffekte oder externe Subscriptions verwalten, profitieren von dedizierten Tests. Das renderHook-Utility von React Testing Library verarbeitet Hook-Lifecycle und Updates.

Ein Debounced-Search-Hook koordiniert mit einer API:

useDebounceSearch.tstypescript
import { useState, useEffect, useCallback } from 'react'

export function useDebounceSearch<T>(
  searchFn: (query: string) => Promise<T[]>,
  delay = 300
) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<T[]>([])
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    if (!query) {
      setResults([])
      return
    }

    setIsLoading(true)
    const timeoutId = setTimeout(async () => {
      try {
        const data = await searchFn(query)
        setResults(data)
      } finally {
        setIsLoading(false)
      }
    }, delay)

    return () => clearTimeout(timeoutId)
  }, [query, searchFn, delay])

  return { query, setQuery, results, isLoading }
}

Tests verifizieren Debouncing-Verhalten und Zustandsübergänge:

useDebounceSearch.test.tstypescript
import { renderHook, act, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useDebounceSearch } from './useDebounceSearch'

describe('useDebounceSearch', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  it('debounces search calls', async () => {
    const mockSearch = vi.fn().mockResolvedValue(['result1', 'result2'])
    const { result } = renderHook(() => useDebounceSearch(mockSearch, 300))

    // Type multiple characters quickly
    act(() => result.current.setQuery('r'))
    act(() => result.current.setQuery('re'))
    act(() => result.current.setQuery('rea'))
    act(() => result.current.setQuery('reac'))
    act(() => result.current.setQuery('react'))

    // Search not called yet (debouncing)
    expect(mockSearch).not.toHaveBeenCalled()

    // Advance timers past debounce delay
    await act(async () => {
      vi.advanceTimersByTime(300)
    })

    // Only one search call with final query
    expect(mockSearch).toHaveBeenCalledTimes(1)
    expect(mockSearch).toHaveBeenCalledWith('react')
  })

  it('shows loading state during search', async () => {
    let resolveSearch: (value: string[]) => void
    const mockSearch = vi.fn().mockImplementation(
      () => new Promise((resolve) => { resolveSearch = resolve })
    )
    const { result } = renderHook(() => useDebounceSearch(mockSearch, 100))

    act(() => result.current.setQuery('test'))
    
    await act(async () => {
      vi.advanceTimersByTime(100)
    })

    expect(result.current.isLoading).toBe(true)

    await act(async () => {
      resolveSearch(['result'])
    })

    expect(result.current.isLoading).toBe(false)
    expect(result.current.results).toEqual(['result'])
  })
})

Fake Timers von Vitest ermöglichen deterministisches Testen von zeitabhängigem Verhalten ohne tatsächliche Verzögerungen.

Interview-Fragen zu React Testing

Technische Interviews prüfen häufig Testing-Wissen. Diese Fragen bewerten praktisches Verständnis statt theoretischer Konzepte.

Häufiger Fehler

Das Testen von Implementierungsdetails wie Zustandswerten oder Komponentenmethoden sollte vermieden werden. Tests, die bei internem Struktur-Refactoring brechen, bieten falsches Vertrauen und Wartungsaufwand.

Abfrage-Priorität in React Testing Library

Die empfohlene Abfrage-Priorität folgt der Accessibility: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Rollenbasierte Abfragen stellen sicher, dass Komponenten zugänglich bleiben und Tests Markup-Änderungen überleben.

Wann findBy vs waitFor verwenden

findBy-Abfragen warten darauf, dass Elemente im DOM erscheinen und geben ein Promise zurück. Sie werden verwendet, wenn nach asynchronen Operationen neue Elemente erwartet werden. waitFor umschließt Assertions, die möglicherweise nicht sofort bestehen, und wiederholt sie bis zum Erfolg oder Timeout. waitFor wird verwendet, wenn Zustandsänderungen an bestehenden Elementen getestet werden.

Mocking fetch vs MSW

Direktes Fetch-Mocking (vi.spyOn(global, 'fetch')) funktioniert für einfache Fälle, erfordert aber die Neuimplementierung von Response-Objekten und Fehlerbehandlung. MSW fängt auf Netzwerkebene ab und ermöglicht Tests, die tatsächliche Fetch-Aufrufe, Request-Serialisierung und Fehlerbehandlungs-Codepfade ausführen.

Für tiefere Praxis mit diesen Konzepten sind die React Testing Interview-Fragen auf SharpSkill zu empfehlen.

Fazit

  • Vitest mit jsdom-Umgebung, Setup-Dateien für Matcher-Erweiterungen und Coverage-Reporting konfigurieren für Sichtbarkeit in Testabdeckungslücken
  • Komponenten nach zugänglichen Rollen und Labels abfragen, um Tests zu schreiben, die benutzerseitiges Verhalten verifizieren und echte Fehler erkennen
  • findBy-Abfragen für Elemente verwenden, die nach asynchronen Operationen erscheinen, waitFor für Assertions auf sich ändernden Zustand
  • Auf der angemessenen Ebene mocken: Modul-Mocks für Unit-Isolation, MSW für realistische Netzwerkschicht-Tests
  • Custom Hooks mit renderHook und Fake Timers testen, um komplexes State-Management und Seiteneffekte zu verifizieren
  • Server Components erfordern entweder asynchrones Rendering in Tests oder vollständige Integrationstests mit Playwright
  • Weiterführende Lektüre: Advanced React Hooks patterns und TypeScript mit React für typsichere Testing-Muster

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#react
#testing
#vitest
#react-testing-library

Teilen

Verwandte Artikel