# Pengujian React di 2026: Vitest, React Testing Library dan Best Practices > Kuasai pengujian React dengan Vitest dan React Testing Library. Pelajari pola pengujian komponen, penanganan async, strategi mocking, dan best practices untuk interview 2026. - Published: 2026-07-09 - Updated: 2026-07-09 - Author: SharpSkill - Reading time: 9 min --- Pengujian React di tahun 2026 berpusat pada [Vitest](https://vitest.dev/) sebagai test runner dominan dan [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) (RTL) sebagai standar untuk pengujian komponen. Kombinasi ini memberikan feedback loop yang cepat, dukungan native ES modules, dan pengujian yang memverifikasi perilaku alih-alih detail implementasi. > **Filosofi Pengujian** > > Tulis pengujian yang menyerupai cara pengguna berinteraksi dengan komponen. Query berdasarkan accessible roles, labels, dan teks—bukan berdasarkan CSS classes atau atribut data-testid. Pendekatan ini menangkap bug nyata dan bertahan saat refactoring. ## Konfigurasi Vitest untuk Proyek React Vitest menggantikan Jest sebagai test runner pilihan untuk aplikasi React modern. Dukungan native ES modules menghilangkan overhead transformasi, dan integrasi erat dengan Vite berarti konfigurasi mencerminkan environment development. Konfigurasi Vitest yang production-ready menangani transformasi JSX, path aliases, dan setup test environment tanpa boilerplate: ```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/**'], }, }, }) ``` File setup memperluas matchers dan mengkonfigurasi cleanup antar pengujian: ```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(), })), }) ``` Setup ini menyediakan kondisi bersih untuk setiap pengujian dan menangani mock browser API umum yang tidak ada di jsdom. ## Pengujian Komponen dengan React Testing Library React Testing Library menegakkan pengujian komponen dari perspektif pengguna. Alih-alih memeriksa state internal atau nilai prop, pengujian berinteraksi dengan output yang di-render melalui accessibility queries. Perhatikan komponen pencarian yang memfilter hasil dan menampilkan loading states: ```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}
  • ))}
) } ``` Pengujian memverifikasi perilaku komponen di berbagai interaksi pengguna dan async states: ```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.' ) }) }) ``` Library `userEvent` mensimulasikan interaksi pengguna yang realistis termasuk keyboard events dan focus management, menangkap bug yang terlewat oleh synthetic events. ## Pola Pengujian Async dan waitFor Operasi async memerlukan strategi waiting yang eksplisit. React Testing Library menyediakan query `findBy` yang menggabungkan `getBy` dengan waiting otomatis, ditambah `waitFor` untuk assertions yang kompleks. > **Prioritas Query** > > Utamakan `findBy` daripada `waitFor` + `getBy` saat menunggu elemen muncul. Simpan `waitFor` untuk assertions pada elemen yang sudah ada yang berubah state. Pola async umum muncul saat menguji hooks dan komponen yang melakukan data fetching: ```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() }) ``` Kustomisasi timeout membantu operasi yang lebih lambat tanpa memperlambat seluruh test suite: ```typescript // Increase timeout for slow operations await screen.findByText('Upload complete', {}, { timeout: 5000 }) await waitFor( () => expect(mockSubmit).toHaveBeenCalled(), { timeout: 3000, interval: 100 } ) ``` ## Strategi Mocking: Modules, APIs, dan Hooks Kemampuan mocking Vitest menangani dependensi eksternal tanpa mencemari module graph. Mocking strategis mengisolasi komponen dari network calls, third-party libraries, dan dependensi kompleks. Module mocking menggantikan imports di level bundler: ```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', }) }) }) ``` Untuk [React hooks yang bergantung pada context atau external state](https://testing-library.com/docs/react-testing-library/api/#renderhook), utilitas `renderHook` memungkinkan pengujian terisolasi: ```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) menyediakan API mocking di level network, memungkinkan pengujian yang menjalankan actual fetch calls: ```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 } ) }), ] ``` ## Pengujian React 19 Server Components Server Components menghadirkan tantangan pengujian unik karena dieksekusi di server dan streaming HTML ke client. Pengujian unit langsung memerlukan pendekatan berbeda dari pengujian client components. Untuk Server Components yang melakukan data fetching, uji output rendering dengan data sources yang di-mock: ```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') }) }) ``` Integration tests dengan [Playwright](https://playwright.dev/) memberikan coverage paling andal untuk Server Components dengan menguji full rendering pipeline: ```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() }) ``` ## Pengujian Custom Hooks dengan State Kompleks Custom hooks yang mengelola state kompleks, side effects, atau external subscriptions mendapat manfaat dari pengujian khusus. Utilitas `renderHook` dari React Testing Library menangani lifecycle hook dan updates. Perhatikan debounced search hook yang berkoordinasi dengan 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 } } ``` Pengujian memverifikasi perilaku debouncing dan transisi state: ```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']) }) }) ``` Fake timers dari Vitest memungkinkan pengujian deterministik dari perilaku yang bergantung waktu tanpa delay sebenarnya. ## Pertanyaan Interview tentang Pengujian React Interview teknis sering menguji pengetahuan pengujian. Pertanyaan-pertanyaan ini menilai pemahaman praktis daripada konsep teoretis. > **Kesalahan Umum** > > Hindari menguji detail implementasi seperti nilai state atau metode komponen. Pengujian yang rusak saat refactoring struktur internal memberikan keyakinan palsu dan beban maintenance. **Prioritas query di React Testing Library** Prioritas query yang direkomendasikan mengikuti accessibility: `getByRole` > `getByLabelText` > `getByPlaceholderText` > `getByText` > `getByTestId`. Query berbasis role memastikan komponen tetap accessible dan pengujian bertahan dari perubahan markup. **Kapan menggunakan `findBy` vs `waitFor`** Query `findBy` menunggu elemen muncul di DOM dan mengembalikan promise. Gunakan saat mengharapkan elemen baru setelah operasi async. `waitFor` membungkus assertions yang mungkin tidak langsung berhasil, mencoba ulang sampai sukses atau timeout. Gunakan `waitFor` saat melakukan assertion pada perubahan state elemen yang sudah ada. **Mocking fetch vs MSW** Mocking fetch langsung (`vi.spyOn(global, 'fetch')`) berfungsi untuk kasus sederhana tetapi memerlukan reimplementasi objek Response dan error handling. MSW mencegat di level network, memungkinkan pengujian untuk menjalankan actual fetch calls, serialisasi request, dan code paths error handling. Untuk latihan lebih dalam dengan konsep-konsep ini, eksplorasi [pertanyaan interview React Testing](/technologies/react-next/interview-questions/react-testing) di SharpSkill. ## Kesimpulan - Konfigurasi Vitest dengan environment jsdom, setup files untuk matcher extensions, dan coverage reporting untuk visibilitas ke dalam gaps test coverage - Query komponen berdasarkan accessible roles dan labels untuk menulis pengujian yang memverifikasi perilaku user-facing dan menangkap bug nyata - Gunakan query `findBy` untuk elemen yang muncul setelah operasi async, `waitFor` untuk assertions pada state yang berubah - Mock di level yang sesuai: module mocks untuk isolasi unit, MSW untuk pengujian network layer yang realistis - Uji custom hooks dengan `renderHook` dan fake timers untuk memverifikasi state management kompleks dan side effects - Server Components memerlukan baik async rendering dalam pengujian atau full integration tests dengan Playwright - Bacaan terkait: [Pola React Hooks tingkat lanjut](/blog/react-next/advanced-react-hooks-patterns-optimizations) dan [TypeScript dengan React](/technologies/react-next/interview-questions/typescript-react) untuk pola pengujian type-safe --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/react-next/react-testing-2026-vitest-rtl-best-practices