การทดสอบ React ในปี 2026: Vitest, React Testing Library และ Best Practices

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

แผนภาพ workflow การทดสอบ React ในปี 2026 ด้วย Vitest และ React Testing Library

การทดสอบ 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:

vitest.config.tstypescript
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 ระหว่างการทดสอบ:

src/test/setup.tstypescript
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:

SearchResults.tsxtsx
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:

SearchResults.test.tsxtypescript
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 ที่ซับซ้อน

ลำดับความสำคัญของ Query

ให้ความสำคัญกับ findBy มากกว่า waitFor + getBy เมื่อรอ element ปรากฏ เก็บ waitFor ไว้สำหรับ assertions บน element ที่มีอยู่ที่เปลี่ยน state

รูปแบบ async ทั่วไปปรากฏเมื่อทดสอบ hooks และ component ที่ทำ data fetching:

async-patterns.test.tsxtypescript
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 ทั้งหมดช้าลง:

typescript
// 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:

api.test.tstypescript
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 ช่วยให้ทดสอบแยกได้:

useAuth.test.tstypescript
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 จริงได้:

handlers.tstypescript
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:

ServerComponent.test.tsxtypescript
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:

e2e/server-component.spec.tstypescript
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:

useDebounceSearch.tstypescript
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:

useDebounceSearch.test.tstypescript
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

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แชร์

บทความที่เกี่ยวข้อง