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.

Pengujian React di tahun 2026 berpusat pada Vitest sebagai test runner dominan dan React Testing Library (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.
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:
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:
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:
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>
)
}Pengujian memverifikasi perilaku komponen di berbagai interaksi pengguna dan async states:
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.'
)
})
})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.
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:
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()
})Kustomisasi timeout membantu operasi yang lebih lambat tanpa memperlambat seluruh test suite:
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })
await waitFor(
() => expect(mockSubmit).toHaveBeenCalled(),
{ timeout: 3000, interval: 100 }
)Siap menguasai wawancara React / Next.js Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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:
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, utilitas renderHook memungkinkan pengujian terisolasi:
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) menyediakan API mocking di level network, memungkinkan pengujian yang menjalankan actual fetch calls:
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:
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 memberikan coverage paling andal untuk Server Components dengan menguji full rendering pipeline:
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:
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 }
}Pengujian memverifikasi perilaku debouncing dan transisi state:
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.
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 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
findByuntuk elemen yang muncul setelah operasi async,waitForuntuk 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
renderHookdan 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 dan TypeScript dengan React untuk pola pengujian type-safe
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Bagikan
Artikel terkait

Zustand vs Redux Toolkit di 2026: State Manager React Mana yang Dipilih?
Perbandingan praktis 2026 antara Zustand 5 dan Redux Toolkit 2.x untuk manajemen state React: setup, data fetching async dengan RTK Query, performa, dan pertanyaan wawancara.

Server Actions Next.js 16 di 2026: Mutasi, Revalidasi, dan Pertanyaan Wawancara
Cara Server Actions Next.js 16 menangani mutasi, revalidasi, status pending, UI optimistis, dan keamanan, lengkap dengan pertanyaan wawancara yang menguji setiap konsep.

React 19 useEffectEvent dan Activity: API Baru dan Pertanyaan Interview 2026
Pembahasan mendalam useEffectEvent dan komponen Activity di React 19.2. Solusi stale closure, pre-rendering di background, contoh kode, dan pertanyaan interview teknis.