Testowanie React w 2026: Vitest, React Testing Library i najlepsze praktyki
Kompleksowy przewodnik po testowaniu aplikacji React z użyciem Vitest i React Testing Library. Wzorce testowania komponentów, obsługa operacji asynchronicznych, strategie mockowania oraz praktyki przydatne na rozmowach rekrutacyjnych.

Testowanie aplikacji React w 2026 roku koncentruje się na Vitest jako dominującym narzędziu do uruchamiania testów oraz React Testing Library (RTL) jako standardzie testowania komponentów. Ta kombinacja zapewnia szybkie pętle informacji zwrotnej, natywne wsparcie dla modułów ES oraz testy weryfikujące zachowanie zamiast szczegółów implementacji.
Testy powinny odzwierciedlać sposób, w jaki użytkownicy wchodzą w interakcję z komponentami. Zapytania powinny opierać się na dostępnych rolach, etykietach i tekście — nie na klasach CSS czy atrybutach data-testid. Takie podejście wykrywa prawdziwe błędy i jest odporne na refaktoryzację.
Konfiguracja Vitest dla projektów React
Vitest zastąpił Jest jako preferowany runner testów dla nowoczesnych aplikacji React. Natywne wsparcie dla modułów ES eliminuje narzut transformacji, a ścisła integracja z Vite oznacza, że konfiguracja odzwierciedla środowisko deweloperskie.
Produkcyjna konfiguracja Vitest obsługuje transformację JSX, aliasy ścieżek i konfigurację środowiska testowego bez zbędnego 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/**'],
},
},
})Plik setup rozszerza matchery i konfiguruje czyszczenie między testami:
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(),
})),
})Ta konfiguracja zapewnia czysty stan dla każdego testu oraz obsługuje typowe mocki API przeglądarki, których brakuje w jsdom.
Testowanie komponentów z React Testing Library
React Testing Library wymusza testowanie komponentów z perspektywy użytkownika. Zamiast inspekcji wewnętrznego stanu czy wartości propsów, testy wchodzą w interakcję z renderowanym wyjściem poprzez zapytania dostępności.
Przykład komponentu wyszukiwania filtrującego wyniki i wyświetlającego stan ładowania:
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>
)
}Testy weryfikują zachowanie komponentu podczas interakcji użytkownika i stanów asynchronicznych:
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.'
)
})
})Biblioteka userEvent symuluje realistyczne interakcje użytkownika, włącznie ze zdarzeniami klawiatury i zarządzaniem focusem, wykrywając błędy pomijane przez zdarzenia syntetyczne.
Wzorce testowania asynchronicznego i waitFor
Operacje asynchroniczne wymagają jawnych strategii oczekiwania. React Testing Library dostarcza zapytania findBy, które łączą getBy z automatycznym oczekiwaniem, oraz waitFor dla złożonych asercji.
Preferuj findBy nad waitFor + getBy podczas oczekiwania na pojawienie się elementów. Rezerwuj waitFor dla asercji na istniejących elementach, które zmieniają stan.
Typowe wzorce asynchroniczne pojawiają się podczas testowania hooków i komponentów pobierających dane:
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()
})Dostosowanie timeout pomaga przy wolniejszych operacjach bez spowalniania całego zestawu testów:
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })
await waitFor(
() => expect(mockSubmit).toHaveBeenCalled(),
{ timeout: 3000, interval: 100 }
)Gotowy na rozmowy o React / Next.js?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Strategie mockowania: moduły, API i hooki
Możliwości mockowania Vitest obsługują zewnętrzne zależności bez zanieczyszczania grafu modułów. Strategiczne mockowanie izoluje komponenty od wywołań sieciowych, bibliotek zewnętrznych i złożonych zależności.
Mockowanie modułów zastępuje importy na poziomie bundlera:
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',
})
})
})Dla hooków React zależnych od kontekstu lub stanu zewnętrznego, narzędzie renderHook umożliwia izolowane testowanie:
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) zapewnia mockowanie API na poziomie sieci, umożliwiając testy wykonujące rzeczywiste wywołania fetch:
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 }
)
}),
]Testowanie Server Components w React 19
Server Components stanowią unikalne wyzwanie testowe, ponieważ wykonują się na serwerze i strumieniują HTML do klienta. Bezpośrednie testy jednostkowe wymagają innego podejścia niż testowanie komponentów klienckich.
Dla Server Components wykonujących pobieranie danych, należy testować wyjście renderowania z mockami źródeł danych:
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')
})
})Testy integracyjne z Playwright zapewniają najbardziej niezawodne pokrycie dla Server Components, testując pełny pipeline renderowania:
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()
})Testowanie custom hooków ze złożonym stanem
Custom hooki zarządzające złożonym stanem, efektami ubocznymi lub zewnętrznymi subskrypcjami korzystają z dedykowanych testów. Narzędzie renderHook z React Testing Library obsługuje cykl życia hooka i aktualizacje.
Przykład hooka debounced search koordynującego się z 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 }
}Testy weryfikują zachowanie debouncingu i przejścia stanów:
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'])
})
})Fałszywe timery z Vitest umożliwiają deterministyczne testowanie zachowań zależnych od czasu bez rzeczywistych opóźnień.
Pytania rekrutacyjne dotyczące testowania React
Rozmowy techniczne często badają wiedzę o testowaniu. Te pytania oceniają praktyczne zrozumienie, a nie koncepcje teoretyczne.
Należy unikać testowania szczegółów implementacji, takich jak wartości stanu czy metody komponentów. Testy, które psują się podczas refaktoryzacji wewnętrznej struktury, dają fałszywe poczucie pewności i obciążenie związane z utrzymaniem.
Priorytet zapytań w React Testing Library
Zalecany priorytet zapytań kieruje się dostępnością: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Zapytania oparte na rolach zapewniają dostępność komponentów, a testy przetrwają zmiany w strukturze znaczników.
Kiedy używać findBy vs waitFor
Zapytania findBy czekają na pojawienie się elementów w DOM i zwracają obietnicę. Należy ich używać, oczekując nowych elementów po operacjach asynchronicznych. waitFor opakowuje asercje, które mogą nie przejść natychmiast, ponawiając próby aż do sukcesu lub timeout. Należy używać waitFor podczas asercji na zmianach stanu istniejących elementów.
Mockowanie fetch vs MSW
Bezpośrednie mockowanie fetch (vi.spyOn(global, 'fetch')) działa dla prostych przypadków, ale wymaga reimplementacji obiektów Response i obsługi błędów. MSW przechwytuje na poziomie sieci, pozwalając testom wykonywać rzeczywiste wywołania fetch, serializację żądań i ścieżki obsługi błędów.
Dla głębszej praktyki z tymi koncepcjami, warto zapoznać się z pytaniami rekrutacyjnymi React Testing na SharpSkill.
Podsumowanie
- Konfiguracja Vitest ze środowiskiem jsdom, plikami setup dla rozszerzeń matcherów i raportowaniem pokrycia dla widoczności luk w pokryciu testowym
- Zapytania o komponenty przez dostępne role i etykiety pozwalają pisać testy weryfikujące zachowanie widoczne dla użytkownika i wykrywające prawdziwe błędy
- Używanie zapytań
findBydla elementów pojawiających się po operacjach asynchronicznych,waitFordla asercji na zmieniającym się stanie - Mockowanie na odpowiednim poziomie: mocki modułów dla izolacji jednostkowej, MSW dla realistycznego testowania warstwy sieciowej
- Testowanie custom hooków z
renderHooki fałszywymi timerami w celu weryfikacji złożonego zarządzania stanem i efektów ubocznych - Server Components wymagają albo asynchronicznego renderowania w testach, albo pełnych testów integracyjnych z Playwright
- Powiązana lektura: Zaawansowane wzorce React Hooks i TypeScript z React dla wzorców testowania z typami
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

Cache Components w Next.js 16: use cache, PPR i pytania rekrutacyjne na 2026 rok
Kompletny przewodnik po Cache Components w Next.js 16: dyrektywa use cache, Partial Pre-Rendering, cacheLife, cacheTag, bezpieczeństwo z use cache private oraz pytania na rozmowę kwalifikacyjną.

React Compiler w 2026: automatyczna memoizacja i pytania rekrutacyjne
Kompletny przewodnik po React Compiler — automatyczna memoizacja, pipeline kompilacji, reguły React, integracja z ESLint i pytania na rozmowy kwalifikacyjne dla React w 2026 roku.

Server Actions w Next.js 16 w 2026: mutacje, rewalidacja i pytania rekrutacyjne
Jak Server Actions w Next.js 16 obsługują mutacje, rewalidację, stan oczekiwania, optymistyczny interfejs i bezpieczeństwo, wraz z pytaniami rekrutacyjnymi sprawdzającymi każdą koncepcję.