React Testing Library ile Test Yazma Rehberi

React Testing Library ile Test Yazma Rehberi
Test yazmak, güvenilir ve sürdürülebilir React uygulamaları geliştirmenin vazgeçilmez bir parçasıdır. React Testing Library (RTL), componentlerinizi kullanıcının bakış açısından test etmenizi sağlayan, topluluk tarafından en çok benimsenen test kütüphanesidir. Bu rehberde, RTL'in temel felsefesinden ileri test tekniklerine kadar her şeyi adım adım ele alacağız.
React Testing Library Felsefesi
RTL'in temel prensibi şudur: "Testleriniz, yazılımınızın kullanılma şekline ne kadar benzerse, o kadar güvenilir olur." Bu nedenle RTL, implementasyon detaylarını (state, props, lifecycle) test etmek yerine, kullanıcının gördüğü ve etkileşimde bulunduğu şeyleri test etmeyi teşvik eder. DOM node'larına erişim için role, label ve metin bazlı sorgular kullanılır.
Kurulum ve Yapılandırma
// Gerekli paketlerin yüklenmesi
// npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
css: true,
},
});
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
Temel Test Yazma
İlk olarak basit bir component testi yazalım. RTL'de en çok kullanılan sorgular getByRole, getByText ve getByLabelText'tir. Bu sorgular erişilebilirlik (a11y) ağacını kullanarak DOM elemanlarını bulur.
// src/components/LoginForm.tsx
import { useState } from 'react';
interface LoginFormProps {
onSubmit: (email: string, password: string) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!email || !password) {
setError('E-posta ve şifre zorunludur');
return;
}
setIsLoading(true);
try {
await onSubmit(email, password);
} catch (err) {
setError('Giriş başarısız. Bilgilerinizi kontrol edin.');
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} aria-label="Giriş formu">
{error && <div role="alert">{error}</div>}
<label htmlFor="email">E-posta</label>
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<label htmlFor="password">Şifre</label>
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit" disabled={isLoading}>
{isLoading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
</button>
</form>
);
}
// src/components/__tests__/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from '../LoginForm';
describe('LoginForm', () => {
const mockOnSubmit = vi.fn();
beforeEach(() => {
mockOnSubmit.mockReset();
});
it('formu doğru şekilde render eder', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText('E-posta')).toBeInTheDocument();
expect(screen.getByLabelText('Şifre')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Giriş Yap' })).toBeInTheDocument();
});
it('boş form gönderildiğinde hata mesajı gösterir', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.click(screen.getByRole('button', { name: 'Giriş Yap' }));
expect(screen.getByRole('alert')).toHaveTextContent('E-posta ve şifre zorunludur');
expect(mockOnSubmit).not.toHaveBeenCalled();
});
it('geçerli bilgilerle formu gönderir', async () => {
const user = userEvent.setup();
mockOnSubmit.mockResolvedValueOnce(undefined);
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('E-posta'), 'test@example.com');
await user.type(screen.getByLabelText('Şifre'), 'password123');
await user.click(screen.getByRole('button', { name: 'Giriş Yap' }));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('test@example.com', 'password123');
});
});
it('gönderim sırasında loading durumunu gösterir', async () => {
const user = userEvent.setup();
mockOnSubmit.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('E-posta'), 'test@example.com');
await user.type(screen.getByLabelText('Şifre'), 'password123');
await user.click(screen.getByRole('button', { name: 'Giriş Yap' }));
expect(screen.getByRole('button', { name: 'Giriş yapılıyor...' })).toBeDisabled();
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Giriş Yap' })).toBeEnabled();
});
});
});
userEvent kütüphanesini fireEvent yerine tercih edin. userEvent, gerçek kullanıcı etkileşimlerini (focus, blur, keydown, keyup) simüle eder ve daha güvenilir testler yazmanızı sağlar.
Asenkron İşlemleri Test Etme
// src/components/__tests__/UserProfile.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserProfile } from '../UserProfile';
// fetch'i mock'la
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('UserProfile', () => {
beforeEach(() => {
mockFetch.mockReset();
});
it('kullanıcı verisi yüklendikten sonra profili gösterir', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
name: 'Tarık Tunç',
email: 'tarik@example.com',
role: 'Developer',
}),
});
render(<UserProfile userId="123" />);
// Yükleniyor durumunu kontrol et
expect(screen.getByText('Yükleniyor...')).toBeInTheDocument();
// Veri geldikten sonra profili kontrol et
expect(await screen.findByText('Tarık Tunç')).toBeInTheDocument();
expect(screen.getByText('tarik@example.com')).toBeInTheDocument();
expect(screen.queryByText('Yükleniyor...')).not.toBeInTheDocument();
});
it('hata durumunda hata mesajı gösterir', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
render(<UserProfile userId="123" />);
expect(await screen.findByRole('alert')).toHaveTextContent('Profil yüklenemedi');
});
});
getByTestId kullanmaktan mümkün olduğunca kaçının. Bu sorgu son çare olmalıdır. Önce getByRole, getByLabelText, getByText gibi semantik sorguları deneyin. Bu hem testlerinizi daha güvenilir yapar hem de component'inizin erişilebilirliğini doğrular.
Custom Hook Testleri
// src/hooks/__tests__/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { useCounter } from '../useCounter';
describe('useCounter', () => {
it('başlangıç değeriyle başlar', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increment doğru çalışır', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('reset başlangıç değerine döner', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(7);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
Sonuç
React Testing Library, "kullanıcı gibi test et" felsefesiyle güvenilir ve sürdürülebilir testler yazmanızı sağlar. Implementasyon detaylarından uzak durarak, component'lerinizin refactoring'e dayanıklı testlerini yazabilirsiniz. userEvent ile gerçekçi etkileşimler, waitFor ile asenkron bekleme ve role bazlı sorgular ile hem testlerinizi hem de component'lerinizin erişilebilirliğini güçlendirin.