Neden Test Yazmalıyız?
Kod yazmak kolaydır. Ama çalışan kod yazmak zordur. Ve 6 ay sonra hala çalışan kod yazmak daha da zordur. İşte testler bu noktada devreye girer.
Test yazmak şu faydaları sağlar:
- Güven: Kod değişikliği yaparken bir şeyi bozmadığınızdan emin olursunuz
- Dokümantasyon: Testler kodun nasıl kullanılacağını gösterir
- Refactor Kolaylığı: Kodu iyileştirirken testler sizi korur
- Hata Tespiti: Bug'lar production'a çıkmadan yakalanır
- Tasarım İyileştirme: Test edilebilir kod, daha modüler ve temiz koddur
"Testler olmadan, kodun çalıştığından emin olamazsınız. Testlerle, sadece testlerin çalıştığından emin olursunuz." - Michael Feathers
Test Türleri
1. Unit Tests (Birim Testleri)
Ne test eder? Tek bir fonksiyon veya sınıfın davranışı
Ne zaman kullanılır? İş mantığı, validasyon, hesaplama fonksiyonları için
Hız: Çok hızlı (milisaniyeler)
function calculateDiscount(price: number, percentage: number): number {
if (price < 0 || percentage < 0 || percentage > 100) {
throw new Error('Invalid input');
}
return price * (percentage / 100);
}
test('calculateDiscount applies percentage correctly', () => {
expect(calculateDiscount(100, 10)).toBe(10);
expect(calculateDiscount(200, 25)).toBe(50);
});
test('calculateDiscount throws on invalid input', () => {
expect(() => calculateDiscount(-100, 10)).toThrow('Invalid input');
expect(() => calculateDiscount(100, 150)).toThrow('Invalid input');
});
2. Integration Tests (Entegrasyon Testleri)
Ne test eder? Birden fazla modülün bir arada çalışması (örn: API + Veritabanı)
Ne zaman kullanılır? API endpoint'leri, veritabanı işlemleri için
Hız: Orta (saniyeler)
describe('POST /users', () => {
it('creates a new user in database', async () => {
const response = await request(app)
.post('/users')
.send({ name: 'Test User', email: 'test@example.com' });
expect(response.status).toBe(201);
expect(response.body.name).toBe('Test User');
const user = await db.users.findOne({ email: 'test@example.com' });
expect(user).toBeTruthy();
});
});
3. E2E Tests (End-to-End Testleri)
Ne test eder? Kullanıcı senaryolarının tamamı (frontend + backend + DB)
Ne zaman kullanılır? Kritik kullanıcı akışları (ödeme, kayıt, vb.)
Hız: Yavaş (dakikalar)
test('user can register and login', async ({ page }) => {
await page.goto('/register');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'SecurePass123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
});
Test Piramidi
İdeal test dağılımı:
- 70% Unit Tests: Hızlı, çok sayıda
- 20% Integration Tests: Kritik entegrasyonlar
- 10% E2E Tests: En önemli kullanıcı akışları
TDD: Red → Green → Refactor
Test Driven Development (TDD), önce test yazip sonra kodu yazmaktır. Süreç üç adımlıdır:
1. RED - Başarısız Test Yaz
İlk olarak, henüz yazılmamış bir özellik için test yazın. Test başarısız olacaktır (kırmızı).
test('formatPhoneNumber formats Turkish phone correctly', () => {
expect(formatPhoneNumber('5551234567')).toBe('+90 555 123 45 67');
});
2. GREEN - Testi Geçecek Minimum Kodu Yaz
Şimdi testi geçirmek için en basit kodu yazın:
function formatPhoneNumber(phone: string): string {
return `+90 ${phone.slice(0, 3)} ${phone.slice(3, 6)} ${phone.slice(6, 8)} ${phone.slice(8)}`;
}
Test yeşile döner ✅
3. REFACTOR - Kodu İyileştir
Şimdi kodu temizleyin, testler hala yeşil kalmalı:
function formatPhoneNumber(phone: string): string {
const cleaned = phone.replace(/\D/g, '');
if (cleaned.length !== 10) {
throw new Error('Phone must be 10 digits');
}
const match = cleaned.match(/^(\d{3})(\d{3})(\d{2})(\d{2})$/);
if (!match) throw new Error('Invalid phone format');
return `+90 ${match[1]} ${match[2]} ${match[3]} ${match[4]}`;
}
TDD Döngüsü Örneği (Tam Süreç)
Bir "todo list" modülü yazalım:
describe('TodoList', () => {
test('starts with empty list', () => {
const list = new TodoList();
expect(list.getAll()).toEqual([]);
});
test('can add a todo', () => {
const list = new TodoList();
list.add('Buy milk');
expect(list.getAll()).toEqual([{ id: 1, text: 'Buy milk', done: false }]);
});
test('can mark todo as done', () => {
const list = new TodoList();
list.add('Buy milk');
list.markDone(1);
expect(list.getAll()[0].done).toBe(true);
});
});
class TodoList {
private todos: Array<{ id: number; text: string; done: boolean }> = [];
private nextId = 1;
getAll() {
return this.todos;
}
add(text: string) {
this.todos.push({ id: this.nextId++, text, done: false });
}
markDone(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = true;
}
}
interface Todo {
id: number;
text: string;
done: boolean;
}
class TodoList {
private todos: Todo[] = [];
private nextId = 1;
getAll(): readonly Todo[] {
return [...this.todos];
}
add(text: string): Todo {
if (!text.trim()) throw new Error('Todo text cannot be empty');
const todo = { id: this.nextId++, text: text.trim(), done: false };
this.todos.push(todo);
return todo;
}
markDone(id: number): void {
const todo = this.todos.find(t => t.id === id);
if (!todo) throw new Error(`Todo with id ${id} not found`);
todo.done = true;
}
}
Jest ile Test Yazma
Kurulum
npm install --save-dev jest @types/jest ts-jest
# Jest config oluştur
npx ts-jest config:init
package.json'a Script Ekle
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Test Çalıştırma
# Tüm testleri çalıştır
npm run test
# Watch mode (değişiklikleri izler, otomatik çalışır)
npm run test:watch
# Coverage raporu
npm run test:coverage
Integration Testing: Test DB Kullanımı
Gerçek veritabanı yerine test veritabanı veya in-memory DB kullanın:
SQLite In-Memory (Hızlı Testing)
import { PrismaClient } from '@prisma/client';
describe('User Repository', () => {
let prisma: PrismaClient;
beforeAll(async () => {
prisma = new PrismaClient({
datasources: { db: { url: 'file::memory:?cache=shared' } }
});
await prisma.$executeRawUnsafe('PRAGMA foreign_keys = ON');
});
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
await prisma.user.deleteMany();
});
it('creates user successfully', async () => {
const user = await prisma.user.create({
data: { name: 'Test', email: 'test@example.com' }
});
expect(user.id).toBeDefined();
expect(user.email).toBe('test@example.com');
});
});
CI Entegrasyonu: GitHub Actions
Her commit'te otomatik test çalıştırın:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Check coverage
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
Test Yazarken Sık Yapılan Hatalar
1. Bağımlı Testler
let userId: number;
test('creates user', () => {
userId = createUser('Test');
expect(userId).toBeDefined();
});
test('updates user', () => {
updateUser(userId, { name: 'Updated' });
});
test('updates user', () => {
const userId = createUser('Test');
updateUser(userId, { name: 'Updated' });
expect(getUser(userId).name).toBe('Updated');
});
2. Aşırı Mocking
jest.mock('./database');
jest.mock('./email');
jest.mock('./logger');
3. Testlerin Implementasyon Detaylarına Bağlanması
test('increments counter', () => {
const counter = new Counter();
counter.increment();
expect(counter._internalValue).toBe(1);
});
test('increments counter', () => {
const counter = new Counter();
counter.increment();
expect(counter.getValue()).toBe(1);
});
İyi Uygulamalar
- AAA Pattern: Arrange (hazırla), Act (çalıştır), Assert (doğrula)
- Açıklayıcı isimlendirme: Test adı ne test ettiğini açıkça belirtmeli
- Tek assertion odağı: Her test tek bir davranışı test etmeli
- Fast & Isolated: Testler hızlı ve birbirinden bağımsız olmalı
- Deterministic: Aynı test her zaman aynı sonucu vermeli (random değerlerden kaçının)
Kaynaklar
- Kent C. Dodds - Testing JavaScript: Ücretsiz test kursu
- Jest Documentation: Resmi döküman
- Test Driven Development by Example (Kent Beck): TDD klasiği
- Testing Library: React component testing
Test yazmak başlangıçta zaman kaybı gibi görünebilir. Ancak zamanla, testlerin size kazandırdığı hız ve güven ortaya çıkar. İyi test edilmiş kod, korkusuzca refactor edilebilir, güvenle deploy edilebilir ve ekip üyeleri için bir güvenlik ağıdır. Test yazmayı alışkanlık haline getirin - geleceğinize en iyi yatırımlardan biridir.
