Тестування React у 2026 році: Vitest, React Testing Library та найкращі практики
Повний посібник з тестування React-додатків за допомогою Vitest та React Testing Library. Патерни тестування компонентів, робота з асинхронними операціями, стратегії мокування та практики для технічних співбесід.

Тестування React у 2026 році зосереджується на Vitest як домінуючому інструменті запуску тестів та React Testing Library (RTL) як стандарті тестування компонентів. Ця комбінація забезпечує швидкі цикли зворотного зв'язку, нативну підтримку ES-модулів та тести, що перевіряють поведінку замість деталей реалізації.
Тести повинні відображати спосіб взаємодії користувачів з компонентами. Запити слід виконувати за доступними ролями, мітками та текстом — а не за CSS-класами чи атрибутами data-testid. Такий підхід виявляє справжні помилки та витримує рефакторинг.
Конфігурація Vitest для React-проєктів
Vitest замінив Jest як переважний раннер тестів для сучасних React-додатків. Нативна підтримка ES-модулів усуває накладні витрати на трансформацію, а тісна інтеграція з Vite означає, що конфігурація відображає середовище розробки.
Продакшн-готова конфігурація Vitest обробляє трансформацію JSX, псевдоніми шляхів та налаштування тестового середовища без зайвого шаблонного коду:
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/**'],
},
},
})Файл налаштування розширює матчери та конфігурує очищення між тестами:
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(),
})),
})Це налаштування забезпечує чистий стан для кожного тесту та обробляє типові моки API браузера, яких немає в jsdom.
Тестування компонентів з React Testing Library
React Testing Library вимагає тестування компонентів з перспективи користувача. Замість інспекції внутрішнього стану чи значень пропсів, тести взаємодіють з відрендереним виводом через запити доступності.
Приклад компонента пошуку, що фільтрує результати та відображає стан завантаження:
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>
)
}Тести перевіряють поведінку компонента під час взаємодії користувача та асинхронних станів:
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.'
)
})
})Бібліотека userEvent симулює реалістичні взаємодії користувача, включаючи події клавіатури та керування фокусом, виявляючи помилки, які пропускають синтетичні події.
Патерни асинхронного тестування та waitFor
Асинхронні операції вимагають явних стратегій очікування. React Testing Library надає запити findBy, що поєднують getBy з автоматичним очікуванням, та waitFor для складних асерцій.
Надавайте перевагу findBy над waitFor + getBy при очікуванні появи елементів. Резервуйте waitFor для асерцій на існуючих елементах, що змінюють стан.
Типові асинхронні патерни з'являються при тестуванні хуків та компонентів, що отримують дані:
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()
})Налаштування таймаутів допомагає з повільнішими операціями без уповільнення всього набору тестів:
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })
await waitFor(
() => expect(mockSubmit).toHaveBeenCalled(),
{ timeout: 3000, interval: 100 }
)Готовий до співбесід з React / Next.js?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Стратегії мокування: модулі, API та хуки
Можливості мокування Vitest обробляють зовнішні залежності без забруднення графа модулів. Стратегічне мокування ізолює компоненти від мережевих викликів, сторонніх бібліотек та складних залежностей.
Мокування модулів замінює імпорти на рівні бандлера:
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',
})
})
})Для React-хуків, що залежать від контексту або зовнішнього стану, утиліта renderHook дозволяє ізольоване тестування:
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) забезпечує мокування API на рівні мережі, дозволяючи тестам виконувати реальні 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 }
)
}),
]Тестування Server Components у React 19
Server Components становлять унікальні виклики для тестування, оскільки виконуються на сервері та стрімлять HTML клієнту. Пряме модульне тестування вимагає іншого підходу, ніж тестування клієнтських компонентів.
Для Server Components, що виконують отримання даних, слід тестувати вивід рендерингу з замоканими джерелами даних:
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')
})
})Інтеграційні тести з Playwright забезпечують найнадійніше покриття для Server Components, тестуючи повний пайплайн рендерингу:
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()
})Тестування кастомних хуків зі складним станом
Кастомні хуки, що керують складним станом, побічними ефектами або зовнішніми підписками, виграють від спеціалізованих тестів. Утиліта renderHook з React Testing Library обробляє життєвий цикл хука та оновлення.
Приклад хука debounced search, що координується з 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 }
}Тести перевіряють поведінку debouncing та переходи станів:
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'])
})
})Фейкові таймери з Vitest дозволяють детерміністичне тестування поведінки, залежної від часу, без реальних затримок.
Питання технічних співбесід щодо тестування React
Технічні співбесіди часто досліджують знання тестування. Ці питання оцінюють практичне розуміння, а не теоретичні концепції.
Слід уникати тестування деталей реалізації, таких як значення стану чи методи компонентів. Тести, що ламаються при рефакторингу внутрішньої структури, дають хибну впевненість та навантаження на супровід.
Пріоритет запитів у React Testing Library
Рекомендований пріоритет запитів слідує доступності: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Запити на основі ролей забезпечують доступність компонентів, а тести витримують зміни розмітки.
Коли використовувати findBy vs waitFor
Запити findBy чекають на появу елементів у DOM та повертають проміс. Їх слід використовувати, очікуючи нові елементи після асинхронних операцій. waitFor обгортає асерції, що можуть не пройти одразу, повторюючи спроби до успіху або таймауту. Слід використовувати waitFor при асерціях на зміни стану існуючих елементів.
Мокування fetch vs MSW
Пряме мокування fetch (vi.spyOn(global, 'fetch')) працює для простих випадків, але вимагає реімплементації об'єктів Response та обробки помилок. MSW перехоплює на рівні мережі, дозволяючи тестам виконувати реальні fetch-виклики, серіалізацію запитів та шляхи обробки помилок.
Для глибшої практики з цими концепціями варто ознайомитися з питаннями співбесід React Testing на SharpSkill.
Підсумок
- Налаштування Vitest із середовищем jsdom, файлами setup для розширення матчерів та звітуванням покриття для видимості прогалин у тестовому покритті
- Запити до компонентів за доступними ролями та мітками дозволяють писати тести, що перевіряють поведінку, видиму користувачу, та виявляють справжні помилки
- Використання запитів
findByдля елементів, що з'являються після асинхронних операцій,waitForдля асерцій на змінюваному стані - Мокування на відповідному рівні: моки модулів для модульної ізоляції, MSW для реалістичного тестування мережевого рівня
- Тестування кастомних хуків з
renderHookта фейковими таймерами для перевірки складного управління станом та побічних ефектів - Server Components вимагають або асинхронного рендерингу в тестах, або повних інтеграційних тестів з Playwright
- Пов'язане читання: Просунуті патерни React Hooks та TypeScript з React для типобезпечних патернів тестування
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Cache Components у Next.js 16: use cache, PPR та питання для співбесід у 2026 році
Повний посібник з Cache Components у Next.js 16: директива use cache, Partial Pre-Rendering, cacheLife, cacheTag, безпека з use cache private та питання для senior-розробників на співбесідах.

React Compiler у 2026 році: автоматична мемоїзація та питання для співбесід
Повний огляд React Compiler — автоматична мемоїзація, конвеєр компіляції, правила React, інтеграція з ESLint та питання для технічних співбесід з React у 2026 році.

Server Actions у Next.js 16 у 2026: мутації, ревалідація та питання співбесід
Як Server Actions у Next.js 16 обробляють мутації, ревалідацію, стан очікування, оптимістичний інтерфейс і безпеку, разом із питаннями співбесід на кожну концепцію.