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-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.
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:
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:
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:
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:
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.
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:
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:
// 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:
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:
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:
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:
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:
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:
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:
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.
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,waitForfü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
renderHookund 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
Teilen
Verwandte Artikel

React 19 useEffectEvent und Activity: Neue APIs und Interviewfragen 2026
Ausführlicher Leitfaden zu useEffectEvent und der Activity-Komponente in React 19.2 – mit Codebeispielen, Best Practices und häufigen Interviewfragen für 2026.

Next.js 16 Cache Components 2026: use cache, PPR und Interview-Fragen für erfahrene Entwickler
Next.js 16 Cache Components im Detail erklärt: use cache Direktive, Partial Pre-Rendering, cacheLife, cacheTag, Sicherheit mit use cache private und typische Interview-Fragen für Senior-Entwickler 2026.

React Compiler 2026: Automatische Memoization und Interview-Fragen
Der React Compiler v1.0 bringt automatische Memoization in React-Anwendungen. Dieser Artikel behandelt die Kompilierungs-Pipeline, die Rules of React, ESLint-Integration und häufige Interview-Fragen zur React-Performance 2026.