2026'da React Testleri: Vitest, React Testing Library ve En İyi Uygulamalar

Vitest ve React Testing Library ile React test yazımında uzmanlaşın. Bileşen test kalıpları, asenkron işlem yönetimi, mock stratejileri ve mülakat için hazır en iyi uygulamalar.

2026'da Vitest ve React Testing Library ile React test iş akışı diyagramı

2026 yılında React testi, baskın test çalıştırıcısı olarak Vitest ve bileşen testi standardı olarak React Testing Library (RTL) üzerine odaklanmaktadır. Bu kombinasyon hızlı geri bildirim döngüleri, yerel ES modül desteği ve uygulama detayları yerine davranışı doğrulayan testler sunar.

Test Felsefesi

Kullanıcıların bileşenlerle nasıl etkileşime girdiğini yansıtan testler yazılmalıdır. Erişilebilir roller, etiketler ve metin üzerinden sorgulama yapılmalı — CSS sınıfları veya data-testid nitelikleri üzerinden değil. Bu yaklaşım gerçek hataları yakalar ve refaktörizasyona dayanıklıdır.

React Projeleri için Vitest Yapılandırması

Vitest, modern React uygulamaları için tercih edilen test çalıştırıcısı olarak Jest'in yerini aldı. Yerel ES modül desteği dönüşüm yükünü ortadan kaldırır ve Vite ile sıkı entegrasyon, yapılandırmanın geliştirme ortamını yansıtması anlamına gelir.

Üretime hazır bir Vitest yapılandırması, JSX dönüşümü, yol takma adları ve test ortamı kurulumunu gereksiz şablon kod olmadan yönetir:

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/**'],
    },
  },
})

Kurulum dosyası eşleştiricileri genişletir ve testler arasında temizlik yapılandırır:

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(),
  })),
})

Bu kurulum her test için temiz bir durum sağlar ve jsdom'da bulunmayan yaygın tarayıcı API mock'larını yönetir.

React Testing Library ile Bileşen Testi

React Testing Library, bileşenlerin kullanıcı perspektifinden test edilmesini zorunlu kılar. İç durumu veya prop değerlerini incelemek yerine, testler erişilebilirlik sorguları aracılığıyla render edilmiş çıktıyla etkileşime girer.

Sonuçları filtreleyen ve yükleme durumlarını gösteren bir arama bileşeni örneği:

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>
  )
}

Testler, kullanıcı etkileşimleri ve asenkron durumlar boyunca bileşenin davranışını doğrular:

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 kütüphanesi, klavye olayları ve odak yönetimi dahil gerçekçi kullanıcı etkileşimlerini simüle eder ve sentetik olayların kaçırdığı hataları yakalar.

Asenkron Test Kalıpları ve waitFor

Asenkron işlemler açık bekleme stratejileri gerektirir. React Testing Library, getBy ile otomatik beklemeyi birleştiren findBy sorgularını ve karmaşık doğrulamalar için waitFor sunar.

Sorgu Önceliği

Elementlerin görünmesini beklerken waitFor + getBy yerine findBy tercih edilmelidir. waitFor, durum değiştiren mevcut elementler üzerindeki doğrulamalar için ayrılmalıdır.

Veri çekme hook'ları ve bileşenlerini test ederken yaygın asenkron kalıplar ortaya çıkar:

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()
})

Zaman aşımı özelleştirmesi, tüm test paketini yavaşlatmadan daha yavaş işlemlere yardımcı olur:

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 mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Mock Stratejileri: Modüller, API'ler ve Hook'lar

Vitest'in mock yetenekleri, modül grafiğini kirletmeden harici bağımlılıkları yönetir. Stratejik mock'lama, bileşenleri ağ çağrılarından, üçüncü taraf kütüphanelerden ve karmaşık bağımlılıklardan izole eder.

Modül mock'lama, import'ları paketleyici düzeyinde değiştirir:

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',
    })
  })
})

Bağlama veya harici duruma bağlı React hook'ları için renderHook yardımcı programı izole test etmeyi sağlar:

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), ağ düzeyinde API mock'laması sağlar ve gerçek fetch çağrılarını kullanan testleri mümkün kılar:

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 Testi

Server Components, sunucuda çalıştığı ve HTML'i istemciye aktardığı için benzersiz test zorlukları sunar. Doğrudan birim testi, istemci bileşen testinden farklı bir yaklaşım gerektirir.

Veri çekme işlemi yapan Server Components için, mock'lanmış veri kaynaklarıyla render çıktısını test etmek gerekir:

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')
  })
})

Playwright ile entegrasyon testleri, tam render pipeline'ını test ederek Server Components için en güvenilir kapsamı sağlar:

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()
})

Karmaşık Durumlu Özel Hook'ların Testi

Karmaşık durum, yan etkiler veya harici abonelikler yöneten özel hook'lar, ayrılmış testlerden fayda sağlar. React Testing Library'den renderHook yardımcı programı, hook yaşam döngüsünü ve güncellemeleri yönetir.

Bir API ile koordine olan debounced arama hook'u örneği:

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 }
}

Testler debounce davranışını ve durum geçişlerini doğrular:

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'])
  })
})

Vitest'ten sahte zamanlayıcılar, gerçek gecikmeler olmadan zamana bağlı davranışların deterministik testini sağlar.

React Testi Hakkında Mülakat Soruları

Teknik mülakatlar sıklıkla test bilgisini sorgular. Bu sorular teorik kavramlar yerine pratik anlayışı değerlendirir.

Yaygın Tuzak

Durum değerleri veya bileşen yöntemleri gibi uygulama detaylarını test etmekten kaçınılmalıdır. İç yapı refaktörize edildiğinde bozulan testler yanlış güven ve bakım yükü sağlar.

React Testing Library'de sorgu önceliği

Önerilen sorgu önceliği erişilebilirliği takip eder: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Rol tabanlı sorgular bileşenlerin erişilebilir kalmasını sağlar ve testler işaretleme değişikliklerine dayanır.

findBy vs waitFor ne zaman kullanılmalı

findBy sorguları DOM'da elementlerin görünmesini bekler ve bir promise döndürür. Asenkron işlemlerden sonra yeni elementler beklenirken kullanılmalıdır. waitFor hemen geçemeyebilecek doğrulamaları sarar, başarı veya zaman aşımına kadar yeniden dener. Mevcut elementlerdeki durum değişikliklerini doğrularken waitFor kullanılmalıdır.

Fetch mock'lama vs MSW

Doğrudan fetch mock'lama (vi.spyOn(global, 'fetch')) basit durumlar için çalışır ancak Response nesnelerinin ve hata yönetiminin yeniden uygulanmasını gerektirir. MSW ağ düzeyinde yakalar, testlerin gerçek fetch çağrılarını, istek serileştirmesini ve hata yönetimi kod yollarını kullanmasına izin verir.

Bu kavramlarla daha derin pratik için SharpSkill'deki React Testing mülakat sorularını inceleyebilirsiniz.

Sonuç

  • Vitest'i jsdom ortamı, eşleştirici uzantıları için kurulum dosyaları ve test kapsam boşluklarında görünürlük için kapsam raporlaması ile yapılandırın
  • Bileşenleri erişilebilir roller ve etiketlerle sorgulayarak kullanıcıya yönelik davranışı doğrulayan ve gerçek hataları yakalayan testler yazın
  • Asenkron işlemlerden sonra görünen elementler için findBy sorgularını, değişen durum üzerindeki doğrulamalar için waitFor kullanın
  • Uygun düzeyde mock'layın: birim izolasyonu için modül mock'ları, gerçekçi ağ katmanı testi için MSW
  • Karmaşık durum yönetimini ve yan etkileri doğrulamak için renderHook ve sahte zamanlayıcılarla özel hook'ları test edin
  • Server Components, testlerde asenkron render veya Playwright ile tam entegrasyon testleri gerektirir
  • İlgili okuma: Tip güvenli test kalıpları için Gelişmiş React Hooks kalıpları ve React ile TypeScript

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#react
#testing
#vitest
#react-testing-library

Paylaş

İlgili makaleler