การทดสอบ React ในปี 2026: Vitest, React Testing Library และ Best Practices
เชี่ยวชาญการทดสอบ React ด้วย Vitest และ React Testing Library เรียนรู้รูปแบบการทดสอบ component การจัดการ async กลยุทธ์ mocking และ best practices สำหรับการสัมภาษณ์ 2026

การทดสอบ React ในปี 2026 มุ่งเน้นที่ Vitest เป็น test runner หลัก และ React Testing Library (RTL) เป็นมาตรฐานสำหรับการทดสอบ component การผสมผสานนี้ให้ feedback loop ที่รวดเร็ว รองรับ native ES modules และการทดสอบที่ตรวจสอบพฤติกรรมแทนที่จะเป็นรายละเอียด implementation
เขียนการทดสอบที่เลียนแบบวิธีที่ผู้ใช้โต้ตอบกับ component ค้นหาตาม accessible roles, labels, และข้อความ—ไม่ใช่ตาม CSS classes หรือ data-testid attributes วิธีการนี้จับ bug จริงและทนทานต่อการ refactoring
การกำหนดค่า Vitest สำหรับโปรเจกต์ React
Vitest ได้แทนที่ Jest เป็น test runner ที่นิยมสำหรับแอปพลิเคชัน React สมัยใหม่ การรองรับ native ES modules ลด transformation overhead และการผสานรวมกับ Vite อย่างใกล้ชิดหมายความว่าการกำหนดค่าสะท้อน environment การพัฒนา
การกำหนดค่า Vitest ระดับ production จัดการ JSX transformation, path aliases, และการตั้งค่า test environment โดยไม่ต้องมี 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/**'],
},
},
})ไฟล์ setup ขยาย matchers และกำหนดค่า cleanup ระหว่างการทดสอบ:
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(),
})),
})การตั้งค่านี้ให้สถานะที่สะอาดสำหรับแต่ละการทดสอบและจัดการ mock browser API ทั่วไปที่ jsdom ขาด
การทดสอบ Component ด้วย React Testing Library
React Testing Library บังคับใช้การทดสอบ component จากมุมมองของผู้ใช้ แทนที่จะตรวจสอบ state ภายในหรือค่า prop การทดสอบโต้ตอบกับ output ที่ render ผ่าน accessibility queries
พิจารณา component ค้นหาที่กรองผลลัพธ์และแสดง 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>
)
}การทดสอบตรวจสอบพฤติกรรมของ component ผ่านการโต้ตอบของผู้ใช้และ 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.'
)
})
})ไลบรารี userEvent จำลองการโต้ตอบของผู้ใช้จริงรวมถึง keyboard events และ focus management จับ bug ที่ synthetic events พลาด
รูปแบบการทดสอบ Async และ waitFor
การดำเนินการ async ต้องการกลยุทธ์การรอที่ชัดเจน React Testing Library ให้ query findBy ที่รวม getBy กับการรออัตโนมัติ บวกกับ waitFor สำหรับ assertions ที่ซับซ้อน
ให้ความสำคัญกับ findBy มากกว่า waitFor + getBy เมื่อรอ element ปรากฏ เก็บ waitFor ไว้สำหรับ assertions บน element ที่มีอยู่ที่เปลี่ยน state
รูปแบบ async ทั่วไปปรากฏเมื่อทดสอบ hooks และ component ที่ทำ 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()
})การปรับแต่ง timeout ช่วยการดำเนินการที่ช้าลงโดยไม่ทำให้ test suite ทั้งหมดช้าลง:
// 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 และแบบทดสอบเทคนิคครับ
กลยุทธ์ Mocking: Modules, APIs, และ Hooks
ความสามารถ mocking ของ Vitest จัดการ dependency ภายนอกโดยไม่ทำให้ module graph ปนเปื้อน การ mock เชิงกลยุทธ์แยก component จาก network calls, ไลบรารีภายนอก, และ dependency ที่ซับซ้อน
Module mocking แทนที่ imports ที่ระดับ 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',
})
})
})สำหรับ React hooks ที่พึ่งพา context หรือ external state ยูทิลิตี้ 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 mocking ที่ระดับ network ช่วยให้การทดสอบสามารถทำ 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 }
)
}),
]การทดสอบ React 19 Server Components
Server Components นำเสนอความท้าทายในการทดสอบที่เป็นเอกลักษณ์เนื่องจากทำงานบน server และ stream HTML ไปยัง client การทดสอบยูนิตโดยตรงต้องการวิธีการที่แตกต่างจากการทดสอบ client component
สำหรับ Server Components ที่ทำ data fetching ให้ทดสอบ output การ render ด้วย data sources ที่ 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 ด้วย Playwright ให้ coverage ที่น่าเชื่อถือที่สุดสำหรับ Server Components โดยทดสอบ 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()
})การทดสอบ Custom Hooks ที่มี State ซับซ้อน
Custom hooks ที่จัดการ state ซับซ้อน, side effects, หรือ external subscriptions ได้ประโยชน์จากการทดสอบเฉพาะ ยูทิลิตี้ renderHook จาก React Testing Library จัดการ lifecycle ของ hook และ updates
พิจารณา debounced search hook ที่ประสานงานกับ 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 และการเปลี่ยน 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 จาก Vitest ช่วยให้ทดสอบแบบ deterministic ของพฤติกรรมที่ขึ้นกับเวลาโดยไม่มี delay จริง
คำถามสัมภาษณ์เกี่ยวกับการทดสอบ React
การสัมภาษณ์ทางเทคนิคมักตรวจสอบความรู้ด้านการทดสอบ คำถามเหล่านี้ประเมินความเข้าใจเชิงปฏิบัติมากกว่าแนวคิดทฤษฎี
หลีกเลี่ยงการทดสอบรายละเอียด implementation เช่นค่า state หรือ method ของ component การทดสอบที่พังเมื่อ refactoring โครงสร้างภายในให้ความมั่นใจที่ผิดและภาระในการบำรุงรักษา
ลำดับความสำคัญของ query ใน React Testing Library
ลำดับความสำคัญของ query ที่แนะนำตาม accessibility: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId Query ตาม role ทำให้ component ยังคง accessible และการทดสอบทนทานต่อการเปลี่ยนแปลง markup
เมื่อใดควรใช้ findBy vs waitFor
Query findBy รอ element ปรากฏใน DOM และคืนค่า promise ใช้เมื่อคาดหวัง element ใหม่หลังการดำเนินการ async waitFor ครอบ assertions ที่อาจไม่ผ่านทันที พยายามซ้ำจนกว่าจะสำเร็จหรือ timeout ใช้ waitFor เมื่อ assert การเปลี่ยนแปลง state ของ element ที่มีอยู่
Mocking fetch vs MSW
การ mock fetch โดยตรง (vi.spyOn(global, 'fetch')) ใช้งานได้สำหรับกรณีง่ายๆ แต่ต้อง reimplementing Response objects และ error handling MSW ดักจับที่ระดับ network ช่วยให้การทดสอบสามารถทำ fetch calls จริง, request serialization, และ code paths ของ error handling
สำหรับการฝึกฝนเชิงลึกกับแนวคิดเหล่านี้ สำรวจ คำถามสัมภาษณ์ React Testing บน SharpSkill
สรุป
- กำหนดค่า Vitest ด้วย jsdom environment, setup files สำหรับ matcher extensions, และ coverage reporting เพื่อให้เห็นช่องว่างของ test coverage
- Query component ตาม accessible roles และ labels เพื่อเขียนการทดสอบที่ตรวจสอบพฤติกรรมที่ผู้ใช้เห็นและจับ bug จริง
- ใช้ query
findByสำหรับ element ที่ปรากฏหลังการดำเนินการ async,waitForสำหรับ assertions บน state ที่เปลี่ยนแปลง - Mock ในระดับที่เหมาะสม: module mocks สำหรับการแยกยูนิต, MSW สำหรับการทดสอบ network layer ที่สมจริง
- ทดสอบ custom hooks ด้วย
renderHookและ fake timers เพื่อตรวจสอบการจัดการ state ที่ซับซ้อนและ side effects - Server Components ต้องการทั้ง async rendering ในการทดสอบหรือ full integration tests ด้วย Playwright
- อ่านเพิ่มเติม: รูปแบบ React Hooks ขั้นสูง และ TypeScript กับ React สำหรับรูปแบบการทดสอบ type-safe
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แชร์
บทความที่เกี่ยวข้อง

Zustand กับ Redux Toolkit ในปี 2026: เลือกตัวจัดการ state ของ React ตัวไหนดี?
เปรียบเทียบเชิงปฏิบัติปี 2026 ระหว่าง Zustand 5 กับ Redux Toolkit 2.x สำหรับการจัดการ state ของ React ทั้งการติดตั้ง การดึงข้อมูล async ด้วย RTK Query ประสิทธิภาพ และคำถามสัมภาษณ์

Next.js 16 Server Actions ปี 2026: Mutation, Revalidation และคำถามสัมภาษณ์
Next.js 16 Server Actions จัดการ mutation, revalidation, สถานะ pending, optimistic UI และความปลอดภัยอย่างไร พร้อมคำถามสัมภาษณ์ที่ทดสอบแต่ละแนวคิด

React 19 useEffectEvent และ Activity: API ใหม่พร้อมคำถามสัมภาษณ์งาน 2026
เจาะลึก useEffectEvent และ Activity component ใน React 19.2 แก้ปัญหา stale closure, pre-rendering เบื้องหลัง พร้อมตัวอย่างโค้ดและคำถามสัมภาษณ์