React Testing in 2026: Vitest, React Testing Library en Best Practices
Professionele strategieën voor React testing in 2026 met Vitest als test runner en React Testing Library voor gedragsgebaseerde componenttests.

React testing in 2026 draait om Vitest als dominante test runner en React Testing Library (RTL) als standaard voor het testen van componenten. Deze combinatie biedt snelle feedbackloops, native ES module-ondersteuning en tests die gedrag verifiëren in plaats van implementatiedetails.
Tests dienen geschreven te worden op een manier die simuleert hoe gebruikers met componenten omgaan. Query's op toegankelijke rollen, labels en tekst zijn zinvoller dan CSS-klassen of data-testid attributen. Deze aanpak detecteert echte bugs en overleeft refactoring.
Vitest Configuratie voor React Projecten
Vitest heeft Jest vervangen als voorkeurstest runner voor moderne React applicaties. Native ES module-ondersteuning elimineert transformatie-overhead, en de nauwe Vite-integratie betekent dat de configuratie de ontwikkelomgeving weerspiegelt.
Een productierijpe Vitest configuratie verwerkt JSX transformatie, pad-aliassen en testomgeving setup zonder 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/**'],
},
},
})Het setup-bestand breidt matchers uit en configureert opruiming tussen 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(),
})),
})Deze setup biedt een schone basis voor elke test en behandelt veelvoorkomende browser API mocks die jsdom niet levert.
Component Testen met React Testing Library
React Testing Library dwingt het testen van componenten vanuit het perspectief van de gebruiker af. In plaats van interne state of prop-waarden te inspecteren, interageren tests met de gerenderde output via toegankelijkheidsquery's.
Een zoekcomponent filtert resultaten en toont laadstaten:
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 verifiëren het gedrag van het component over gebruikersinteracties en asynchrone states heen:
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.'
)
})
})De userEvent bibliotheek simuleert realistische gebruikersinteracties inclusief toetsenbordgebeurtenissen en focusbeheer, en detecteert bugs die synthetische events missen.
Asynchrone Test Patronen en waitFor
Asynchrone operaties vereisen expliciete wachtstrategieën. React Testing Library biedt findBy query's die getBy combineren met automatisch wachten, plus waitFor voor complexe assertions.
Geef de voorkeur aan findBy boven waitFor + getBy bij het wachten op elementen die verschijnen. Reserveer waitFor voor assertions op bestaande elementen die van state veranderen.
Veelvoorkomende asynchrone patronen verschijnen bij het testen van data-fetching hooks en componenten:
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-aanpassing helpt bij langzamere operaties zonder de hele testsuite te vertragen:
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })
await waitFor(
() => expect(mockSubmit).toHaveBeenCalled(),
{ timeout: 3000, interval: 100 }
)Klaar om je React / Next.js gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Mocking Strategieën: Modules, API's en Hooks
De mocking-mogelijkheden van Vitest behandelen externe afhankelijkheden zonder de modulegraph te vervuilen. Strategische mocking isoleert componenten van netwerkaanroepen, third-party bibliotheken en complexe afhankelijkheden.
Module mocking vervangt imports op bundlerniveau:
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',
})
})
})Voor React hooks die afhankelijk zijn van context of externe state, maakt de renderHook utility geïsoleerde tests mogelijk:
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) biedt API mocking op netwerkniveau, waardoor tests daadwerkelijke fetch-aanroepen kunnen uitvoeren:
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 }
)
}),
]React 19 Server Components Testen
Server Components bieden unieke testuitdagingen omdat ze op de server draaien en HTML naar de client streamen. Direct unit testen vereist een andere aanpak dan client component testing.
Voor Server Components die data ophalen, test men de rendering output met gemockte databronnen:
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')
})
})Integratietests met Playwright bieden de meest betrouwbare dekking voor Server Components door de volledige rendering pipeline te 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()
})Custom Hooks Testen met Complexe State
Custom hooks die complexe state, side effects of externe subscriptions beheren, profiteren van dedicated tests. De renderHook utility van React Testing Library behandelt hook lifecycle en updates.
Een debounced search hook coördineert met een 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 verifiëren debouncing-gedrag en state-overgangen:
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 van Vitest maken deterministisch testen van tijdsafhankelijk gedrag mogelijk zonder daadwerkelijke vertragingen.
Sollicitatievragen over React Testing
Technische sollicitatiegesprekken onderzoeken vaak testkennis. Deze vragen beoordelen praktisch begrip in plaats van theoretische concepten.
Vermijd het testen van implementatiedetails zoals state-waarden of componentmethoden. Tests die breken bij het refactoren van interne structuur bieden vals vertrouwen en onderhoudslast.
Query prioriteit in React Testing Library
De aanbevolen query-prioriteit volgt toegankelijkheid: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Rolgebaseerde query's zorgen ervoor dat componenten toegankelijk blijven en tests markup-wijzigingen overleven.
Wanneer findBy vs waitFor gebruiken
findBy query's wachten tot elementen in de DOM verschijnen en retourneren een Promise. Ze worden gebruikt wanneer nieuwe elementen na asynchrone operaties worden verwacht. waitFor wrapt assertions die mogelijk niet direct slagen, en herhaalt tot succes of timeout. waitFor wordt gebruikt bij het testen van state-veranderingen op bestaande elementen.
Fetch mocken vs MSW
Directe fetch mocking (vi.spyOn(global, 'fetch')) werkt voor eenvoudige gevallen maar vereist herimplementatie van Response objecten en error handling. MSW onderschept op netwerkniveau, waardoor tests daadwerkelijke fetch-aanroepen, request serialisatie en error handling codepaden kunnen uitvoeren.
Voor diepere oefening met deze concepten bieden de React Testing sollicitatievragen op SharpSkill uitstekende praktijkoefeningen.
Conclusie
- Configureer Vitest met jsdom-omgeving, setup-bestanden voor matcher-uitbreidingen en coverage-rapportage voor zichtbaarheid in testdekkingsgaten
- Query componenten op toegankelijke rollen en labels om tests te schrijven die gebruikersgerichte gedrag verifiëren en echte bugs detecteren
- Gebruik
findByquery's voor elementen die na asynchrone operaties verschijnen,waitForvoor assertions op veranderende state - Mock op het juiste niveau: module mocks voor unit isolatie, MSW voor realistische netwerklaag testing
- Test custom hooks met
renderHooken fake timers om complex state management en side effects te verifiëren - Server Components vereisen ofwel asynchrone rendering in tests of volledige integratietests met Playwright
- Gerelateerde lectuur: Geavanceerde React Hooks patronen en TypeScript met React voor type-safe testing patronen
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

React 19 useEffectEvent en Activity: Nieuwe API's en Sollicitatievragen 2026
Uitgebreide gids over useEffectEvent en het Activity-component in React 19.2 – met codevoorbeelden, best practices en veelgestelde sollicitatievragen voor 2026.

Next.js 16 Cache Components in 2026: use cache, PPR en Interviewvragen voor Senior Developers
Uitgebreide gids over Next.js 16 Cache Components: de use cache directive, Partial Pre-Rendering, cacheLife, cacheTag, beveiliging met use cache private en technische interviewvragen voor 2026.

React Compiler in 2026: Automatische Memoization en Interviewvragen
De React Compiler v1.0 brengt automatische memoization naar React-applicaties. Dit artikel behandelt de compilatiepijplijn, de Rules of React, ESLint-integratie en veelgestelde interviewvragen over React-performance in 2026.