# การทดสอบ React ในปี 2026: Vitest, React Testing Library และ Best Practices > เชี่ยวชาญการทดสอบ React ด้วย Vitest และ React Testing Library เรียนรู้รูปแบบการทดสอบ component การจัดการ async กลยุทธ์ mocking และ best practices สำหรับการสัมภาษณ์ 2026 - Published: 2026-07-09 - Updated: 2026-07-09 - Author: SharpSkill - Reading time: 9 min --- การทดสอบ React ในปี 2026 มุ่งเน้นที่ [Vitest](https://vitest.dev/) เป็น test runner หลัก และ [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) (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: ```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/**'], }, }, }) ``` ไฟล์ setup ขยาย matchers และกำหนดค่า cleanup ระหว่างการทดสอบ: ```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(), })), }) ``` การตั้งค่านี้ให้สถานะที่สะอาดสำหรับแต่ละการทดสอบและจัดการ mock browser API ทั่วไปที่ jsdom ขาด ## การทดสอบ Component ด้วย React Testing Library React Testing Library บังคับใช้การทดสอบ component จากมุมมองของผู้ใช้ แทนที่จะตรวจสอบ state ภายในหรือค่า prop การทดสอบโต้ตอบกับ output ที่ render ผ่าน accessibility queries พิจารณา component ค้นหาที่กรองผลลัพธ์และแสดง 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}
  • ))}
) } ``` การทดสอบตรวจสอบพฤติกรรมของ component ผ่านการโต้ตอบของผู้ใช้และ 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.' ) }) }) ``` ไลบรารี `userEvent` จำลองการโต้ตอบของผู้ใช้จริงรวมถึง keyboard events และ focus management จับ bug ที่ synthetic events พลาด ## รูปแบบการทดสอบ Async และ waitFor การดำเนินการ async ต้องการกลยุทธ์การรอที่ชัดเจน React Testing Library ให้ query `findBy` ที่รวม `getBy` กับการรออัตโนมัติ บวกกับ `waitFor` สำหรับ assertions ที่ซับซ้อน > **ลำดับความสำคัญของ Query** > > ให้ความสำคัญกับ `findBy` มากกว่า `waitFor` + `getBy` เมื่อรอ element ปรากฏ เก็บ `waitFor` ไว้สำหรับ assertions บน element ที่มีอยู่ที่เปลี่ยน state รูปแบบ async ทั่วไปปรากฏเมื่อทดสอบ hooks และ component ที่ทำ 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() }) ``` การปรับแต่ง timeout ช่วยการดำเนินการที่ช้าลงโดยไม่ทำให้ test suite ทั้งหมดช้าลง: ```typescript // Increase timeout for slow operations await screen.findByText('Upload complete', {}, { timeout: 5000 }) await waitFor( () => expect(mockSubmit).toHaveBeenCalled(), { timeout: 3000, interval: 100 } ) ``` ## กลยุทธ์ Mocking: Modules, APIs, และ Hooks ความสามารถ mocking ของ Vitest จัดการ dependency ภายนอกโดยไม่ทำให้ module graph ปนเปื้อน การ mock เชิงกลยุทธ์แยก component จาก network calls, ไลบรารีภายนอก, และ dependency ที่ซับซ้อน Module mocking แทนที่ imports ที่ระดับ 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', }) }) }) ``` สำหรับ [React hooks ที่พึ่งพา context หรือ external state](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 mocking ที่ระดับ network ช่วยให้การทดสอบสามารถทำ 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 } ) }), ] ``` ## การทดสอบ React 19 Server Components Server Components นำเสนอความท้าทายในการทดสอบที่เป็นเอกลักษณ์เนื่องจากทำงานบน server และ stream HTML ไปยัง client การทดสอบยูนิตโดยตรงต้องการวิธีการที่แตกต่างจากการทดสอบ client component สำหรับ Server Components ที่ทำ data fetching ให้ทดสอบ output การ render ด้วย data sources ที่ 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 ด้วย [Playwright](https://playwright.dev/) ให้ coverage ที่น่าเชื่อถือที่สุดสำหรับ Server Components โดยทดสอบ 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() }) ``` ## การทดสอบ Custom Hooks ที่มี State ซับซ้อน Custom hooks ที่จัดการ state ซับซ้อน, side effects, หรือ external subscriptions ได้ประโยชน์จากการทดสอบเฉพาะ ยูทิลิตี้ `renderHook` จาก React Testing Library จัดการ lifecycle ของ hook และ updates พิจารณา debounced search hook ที่ประสานงานกับ 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 และการเปลี่ยน 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 จาก 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](/technologies/react-next/interview-questions/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 ขั้นสูง](/blog/react-next/advanced-react-hooks-patterns-optimizations) และ [TypeScript กับ React](/technologies/react-next/interview-questions/typescript-react) สำหรับรูปแบบการทดสอบ type-safe --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/react-next/react-testing-2026-vitest-rtl-best-practices