# Тестування React у 2026 році: Vitest, React Testing Library та найкращі практики > Повний посібник з тестування React-додатків за допомогою Vitest та React Testing Library. Патерни тестування компонентів, робота з асинхронними операціями, стратегії мокування та практики для технічних співбесід. - Published: 2026-07-09 - Updated: 2026-07-09 - Author: SharpSkill - Tags: react, testing, vitest, react-testing-library - Reading time: 9 min --- Тестування React у 2026 році зосереджується на [Vitest](https://vitest.dev/) як домінуючому інструменті запуску тестів та [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) (RTL) як стандарті тестування компонентів. Ця комбінація забезпечує швидкі цикли зворотного зв'язку, нативну підтримку ES-модулів та тести, що перевіряють поведінку замість деталей реалізації. > **Філософія тестування** > > Тести повинні відображати спосіб взаємодії користувачів з компонентами. Запити слід виконувати за доступними ролями, мітками та текстом — а не за CSS-класами чи атрибутами data-testid. Такий підхід виявляє справжні помилки та витримує рефакторинг. ## Конфігурація Vitest для React-проєктів Vitest замінив Jest як переважний раннер тестів для сучасних React-додатків. Нативна підтримка ES-модулів усуває накладні витрати на трансформацію, а тісна інтеграція з Vite означає, що конфігурація відображає середовище розробки. Продакшн-готова конфігурація Vitest обробляє трансформацію JSX, псевдоніми шляхів та налаштування тестового середовища без зайвого шаблонного коду: ```typescript // vitest.config.ts 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/**'], }, }, }) ``` Файл налаштування розширює матчери та конфігурує очищення між тестами: ```typescript // src/test/setup.ts 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 вимагає тестування компонентів з перспективи користувача. Замість інспекції внутрішнього стану чи значень пропсів, тести взаємодіють з відрендереним виводом через запити доступності. Приклад компонента пошуку, що фільтрує результати та відображає стан завантаження: ```tsx // SearchResults.tsx import { useState } from 'react' interface SearchResultsProps { onSearch: (query: string) => Promise } export function SearchResults({ onSearch }: SearchResultsProps) { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(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 (
setQuery(e.target.value)} /> {error &&

{error}

}
    {results.map((result, i) => (
  • {result}
  • ))}
) } ``` Тести перевіряють поведінку компонента під час взаємодії користувача та асинхронних станів: ```typescript // SearchResults.test.tsx 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() // 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((resolve) => { resolveSearch = resolve }) const mockSearch = vi.fn().mockReturnValue(searchPromise) const user = userEvent.setup() render() 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() 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` для асерцій на існуючих елементах, що змінюють стан. Типові асинхронні патерни з'являються при тестуванні хуків та компонентів, що отримують дані: ```typescript // async-patterns.test.tsx 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() // 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() // 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() 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() // queryBy returns null instead of throwing, use for absence checks expect(screen.queryByText('Premium')).not.toBeInTheDocument() }) ``` Налаштування таймаутів допомагає з повільнішими операціями без уповільнення всього набору тестів: ```typescript // Increase timeout for slow operations await screen.findByText('Upload complete', {}, { timeout: 5000 }) await waitFor( () => expect(mockSubmit).toHaveBeenCalled(), { timeout: 3000, interval: 100 } ) ``` ## Стратегії мокування: модулі, API та хуки Можливості мокування Vitest обробляють зовнішні залежності без забруднення графа модулів. Стратегічне мокування ізолює компоненти від мережевих викликів, сторонніх бібліотек та складних залежностей. Мокування модулів замінює імпорти на рівні бандлера: ```typescript // api.test.ts 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-хуків, що залежать від контексту або зовнішнього стану](https://testing-library.com/docs/react-testing-library/api/#renderhook), утиліта `renderHook` дозволяє ізольоване тестування: ```typescript // useAuth.test.ts 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 }) => ( {children} ), }) 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-виклики: ```typescript // handlers.ts 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, що виконують отримання даних, слід тестувати вивід рендерингу з замоканими джерелами даних: ```typescript // ServerComponent.test.tsx 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](https://playwright.dev/) забезпечують найнадійніше покриття для Server Components, тестуючи повний пайплайн рендерингу: ```typescript // e2e/server-component.spec.ts 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: ```typescript // useDebounceSearch.ts import { useState, useEffect, useCallback } from 'react' export function useDebounceSearch( searchFn: (query: string) => Promise, delay = 300 ) { const [query, setQuery] = useState('') const [results, setResults] = useState([]) 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 та переходи станів: ```typescript // useDebounceSearch.test.ts 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](/technologies/react-next/interview-questions/react-testing) на SharpSkill. ## Підсумок - Налаштування Vitest із середовищем jsdom, файлами setup для розширення матчерів та звітуванням покриття для видимості прогалин у тестовому покритті - Запити до компонентів за доступними ролями та мітками дозволяють писати тести, що перевіряють поведінку, видиму користувачу, та виявляють справжні помилки - Використання запитів `findBy` для елементів, що з'являються після асинхронних операцій, `waitFor` для асерцій на змінюваному стані - Мокування на відповідному рівні: моки модулів для модульної ізоляції, MSW для реалістичного тестування мережевого рівня - Тестування кастомних хуків з `renderHook` та фейковими таймерами для перевірки складного управління станом та побічних ефектів - Server Components вимагають або асинхронного рендерингу в тестах, або повних інтеграційних тестів з Playwright - Пов'язане читання: [Просунуті патерни React Hooks](/blog/react-next/advanced-react-hooks-patterns-optimizations) та [TypeScript з React](/technologies/react-next/interview-questions/typescript-react) для типобезпечних патернів тестування --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/react-next/react-testing-2026-vitest-rtl-best-practices