TypeScript Test Stratejileri: Vitest ve Jest

TypeScript Test Stratejileri: Vitest ve Jest
Test yazmak, sürdürülebilir yazılım geliştirmenin temel taşıdır. TypeScript projelerinde tip güvenliği ile test kalitesini birleştirmek, hataları hem derleme hem de runtime aşamasında yakalamanızı sağlar. Bu rehberde Vitest ve Jest kullanarak TypeScript projeleriniz için etkili test stratejileri, konfigürasyon, mock'lama ve en iyi uygulamaları inceleyeceğiz.
Vitest vs Jest: Hangisini Seçmeli?
| Özellik | Vitest | Jest |
|---|---|---|
| Hız | Çok hızlı (Vite tabanlı) | Orta |
| TypeScript desteği | Yerleşik (SWC/esbuild) | ts-jest veya @swc/jest gerekir |
| ESM desteği | Doğal | Konfigürasyon gerektirir |
| Watch modu | Çok hızlı HMR | İyi |
| Jest uyumluluğu | Büyük ölçüde uyumlu | - |
| Ekosistem | Büyüyen | Çok geniş |
Vitest Kurulumu
# Kurulum
npm install -D vitest @vitest/coverage-v8 @vitest/ui
# React projeleri için
npm install -D @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
include: ['**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.d.ts', 'src/**/*.test.{ts,tsx}'],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
// tests/setup.ts
import '@testing-library/jest-dom/vitest';
// Global mock'lar
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams(),
}));
Jest ile TypeScript Kurulumu
# Kurulum
npm install -D jest ts-jest @types/jest
# veya SWC ile (daha hızlı)
npm install -D jest @swc/jest @types/jest
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['./tests/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json',
}],
},
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
],
};
export default config;
Unit Test Yazma
// utils/formatters.ts
export function formatCurrency(
amount: number,
currency: string = 'TRY',
locale: string = 'tr-TR'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ş/g, 's')
.replace(/ı/g, 'i')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
// utils/formatters.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency, slugify } from './formatters';
describe('formatCurrency', () => {
it('TRY para birimini doğru formatlar', () => {
expect(formatCurrency(1234.56)).toBe('₺1.234,56');
});
it('USD para birimini doğru formatlar', () => {
const result = formatCurrency(1000, 'USD', 'en-US');
expect(result).toBe('$1,000.00');
});
it('negatif değerleri formatlar', () => {
const result = formatCurrency(-500);
expect(result).toContain('500');
});
});
describe('slugify', () => {
it('Türkçe karakterleri dönüştürür', () => {
expect(slugify('Türkçe Başlık')).toBe('turkce-baslik');
});
it('boşlukları tire ile değiştirir', () => {
expect(slugify('merhaba dünya')).toBe('merhaba-dunya');
});
it('özel karakterleri kaldırır', () => {
expect(slugify('test!@#$%abc')).toBe('test-abc');
});
it('baş ve sondaki tireleri kaldırır', () => {
expect(slugify('--test--')).toBe('test');
});
});
Async Fonksiyon Testleri
// services/userService.ts
interface User {
id: string;
email: string;
name: string;
}
interface UserRepository {
findById(id: string): Promise<User | null>;
create(data: Omit<User, 'id'>): Promise<User>;
}
export class UserService {
constructor(private repo: UserRepository) {}
async getUser(id: string): Promise<User> {
const user = await this.repo.findById(id);
if (!user) {
throw new Error(`User not found: ${id}`);
}
return user;
}
async createUser(email: string, name: string): Promise<User> {
if (!email.includes('@')) {
throw new Error('Geçersiz email');
}
return this.repo.create({ email, name });
}
}
// services/userService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './userService';
describe('UserService', () => {
const mockRepo = {
findById: vi.fn(),
create: vi.fn(),
};
let service: UserService;
beforeEach(() => {
vi.clearAllMocks();
service = new UserService(mockRepo);
});
describe('getUser', () => {
it('kullanıcıyı ID ile döndürür', async () => {
const mockUser = { id: '1', email: 'a@b.com', name: 'Test' };
mockRepo.findById.mockResolvedValue(mockUser);
const user = await service.getUser('1');
expect(user).toEqual(mockUser);
expect(mockRepo.findById).toHaveBeenCalledWith('1');
});
it('kullanıcı bulunamazsa hata fırlatır', async () => {
mockRepo.findById.mockResolvedValue(null);
await expect(service.getUser('999'))
.rejects.toThrow('User not found: 999');
});
});
describe('createUser', () => {
it('geçersiz email ile hata fırlatır', async () => {
await expect(service.createUser('invalid', 'Test'))
.rejects.toThrow('Geçersiz email');
});
it('geçerli bilgilerle kullanıcı oluşturur', async () => {
const mockUser = { id: '1', email: 'a@b.com', name: 'Test' };
mockRepo.create.mockResolvedValue(mockUser);
const result = await service.createUser('a@b.com', 'Test');
expect(result).toEqual(mockUser);
expect(mockRepo.create).toHaveBeenCalledWith({
email: 'a@b.com',
name: 'Test',
});
});
});
});
React Component Testleri
// components/SearchInput.tsx
'use client';
import { useState, useCallback } from 'react';
interface SearchInputProps {
onSearch: (query: string) => void;
placeholder?: string;
}
export function SearchInput({ onSearch, placeholder = 'Ara...' }: SearchInputProps) {
const [value, setValue] = useState('');
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (value.trim()) onSearch(value.trim());
},
[value, onSearch]
);
return (
<form onSubmit={handleSubmit} role="search">
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
aria-label="Arama"
/>
<button type="submit">Ara</button>
</form>
);
}
// components/SearchInput.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { SearchInput } from './SearchInput';
describe('SearchInput', () => {
it('placeholder gösterir', () => {
render(<SearchInput onSearch={vi.fn()} placeholder="Ürün ara..." />);
expect(screen.getByPlaceholderText('Ürün ara...')).toBeInTheDocument();
});
it('form submit edildiğinde onSearch çağrılır', async () => {
const onSearch = vi.fn();
const user = userEvent.setup();
render(<SearchInput onSearch={onSearch} />);
await user.type(screen.getByRole('textbox'), 'typescript');
await user.click(screen.getByRole('button', { name: 'Ara' }));
expect(onSearch).toHaveBeenCalledWith('typescript');
});
it('boş değerle arama yapmaz', async () => {
const onSearch = vi.fn();
const user = userEvent.setup();
render(<SearchInput onSearch={onSearch} />);
await user.click(screen.getByRole('button', { name: 'Ara' }));
expect(onSearch).not.toHaveBeenCalled();
});
});
@testing-library/user-event kütüphanesi, gerçek kullanıcı etkileşimlerini simüle eder ve fireEvent'e göre daha güvenilir testler yazmanızı sağlar.
Tip Testleri
TypeScript tiplerinin doğru çalıştığını test etmek için expect-type kütüphanesini kullanabilirsiniz:
// npm install -D expect-type
import { expectTypeOf, describe, it } from 'vitest';
import { createUser, type User } from './userService';
describe('Tip testleri', () => {
it('createUser doğru tipi döndürür', () => {
expectTypeOf(createUser).returns.resolves.toEqualTypeOf<User>();
});
it('User tipi doğru alanları içerir', () => {
expectTypeOf<User>().toHaveProperty('id');
expectTypeOf<User>().toHaveProperty('email');
});
});
Coverage ve CI Entegrasyonu
// package.json scripts
{
"scripts": {
"test": "vitest",
"test:watch": "vitest --watch",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
# GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run test:coverage
- uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
Sonuç
TypeScript projelerinde etkili test stratejileri, tip güvenliği ile runtime güvenliğini birleştirir. Vitest modern projeler için hız ve kullanım kolaylığı sunarken, Jest geniş ekosistemi ile güçlü bir alternatiftir. Unit testler ile fonksiyon mantığını, integration testler ile servis etkileşimlerini, component testler ile UI davranışını doğrulayın. Coverage hedeflerinizi belirleyin ama sadece anlamlı testler yazın.