Yazılımda Otomatik Test ve TDD Nedir
Test Güdümlü Geliştirme (TDD) ve yazılım test piramidi nedir? Unit (Birim) testi, entegrasyon testi, Playwright/Cypress ile uçtan uca (E2E) otomasyon rehberi.
Yozel
12 dakika

İçindekiler (16)
Yazılım geliştirme dünyasında en pahalı hata, canlı ortama (production) çıktıktan sonra müşteriler tarafından keşfedilen hatadır. IBM Systems Sciences Institute araştırmalarına göre; tasarım ve kodlama aşamasında yakalanan bir hatayı düzeltmenin maliyeti 1 birimken, aynı hatanın canlı ortamda ödeme veya sipariş kaybına yol açtıktan sonra düzeltilme maliyeti 30 ile 100 kat arasında artmaktadır. Üstelik günde onlarca kez kod dağıtımı (CI/CD deployment) yapan modern mühendislik ekiplerinde, her sürüm öncesinde tüm ekranları insan eliyle (manuel) tek tek tıklayarak test etmek matematiksel olarak imkansızdır. İşte yazılım kalitesini şansa bırakmayan, mühendislerin mevcut kodu bozma korkusu yaşamadan cesurca yeniden yapılandırma (refactoring) yapabilmesini sağlayan disipline Otomatik Yazılım Testi (Automated Testing) ve Test Güdümlü Geliştirme (Test-Driven Development - TDD) adı verilir.
Bugün Uygulama ve Yazılım ekosisteminde yüksek güvenilirlikli finansal sistemlerden mikroservis altyapılarına kadar her kurumsal ürün, sağlam bir otomatik test güvenlik ağı üzerinde yükselir. Bu kapsamlı rehberde; Kent Beck'in TDD (Red-Green-Refactor) metodolojisini, Mike Cohn'un Yazılım Test Piramidini, Birim (Unit) ve Entegrasyon testlerinde Mocking ile Testcontainers yaklaşımlarını, Playwright ve Cypress ile Uçtan Uca (E2E) otomasyonu, Tüketici Güdümlü Kontrat Testlerini (Pact) ve k6 ile yük testlerini tüm mühendislik detaylarıyla inceliyoruz.
1. Test Güdümlü Geliştirme (Test-Driven Development - TDD) Nedir?
Kent Beck tarafından Extreme Programming (XP) disiplininin bir parçası olarak geliştirilen Test-Driven Development (TDD - Test Güdümlü Geliştirme), yaygın bilinenin aksine sadece bir "test yazma tekniği" değil; bir yazılım tasarım ve mimari düşünme metodolojisidir. Geleneksel yaklaşımda yazılımcı önce yüzlerce satırlık iş kodunu yazar, ardından (eğer vakti kalırsa) bu kodu doğrulamak için test yazmaya çalışır. Ancak sonradan test yazılmaya çalışıldığında, sıkı bağımlılıklarla (tight coupling) yazılmış kodun test edilemez olduğu fark edilir.
TDD ise bu süreci tamamen tersine çevirir ve kırılmaz bir kural koyar: "Başarısız olan bir otomatik birim testi yazmadan, üretim koduna (production code) tek bir satır bile yeni iş mantığı ekleyemezsin!" TDD süreci, saniyeler ve dakikalar içinde tekrarlanan ritmik Red - Green - Refactor (Kırmızı - Yeşil - Yeniden Yapılandır) döngüsüyle ilerler:
- RED (Kırmızı - Başarısız Test Yaz): Geliştireceğiniz özelliğin veya iş kuralının nasıl çalışması gerektiğini hayal ederek küçük bir birim testi yazarsınız. Henüz ortada gerçek sınıf veya metot olmadığı için test derlenmez veya kırmızı ışık yakarak başarısız olur (Fail). Bu aşama, geliştiriciyi kodu yazmadan önce "Bu sınıfın dış arayüzü (API'si) nasıl olmalı?" sorusu üzerinde düşünmeye zorlar.
- GREEN (Yeşil - Testi Geçecek En Basit Kodu Yaz): Testin yeşil yanmasını (Pass) sağlayacak en kısa, en basit ve en doğrudan üretim kodunu yazarsınız. Bu aşamada mükemmel tasarım veya performans optimizasyonu düşünülmez; tek hedef testin doğruladığı iş kuralını çalıştırmaktır.
- REFACTOR (Mavi / Yeniden Yapılandır - Kodu Temizle): Testiniz artık yeşil yanmaktadır ve arkanızda sizi koruyan bir güvenlik ağı vardır! Şimdi yazdığınız kodu, testin yeşil durumunu bozmadan temizlersiniz: Tekrar eden kodları (duplication) kaldırır, değişken isimlerini iyileştirir, SOLID prensiplerini uygular ve mimariyi rafine edersiniz. Her küçük değişiklikte testi milisaniyeler içinde tekrar çalıştırarak hiçbir şeyi bozmadığınızdan %100 emin olursunuz.
1.1. Birim Testlerinde AAA (Arrange - Act - Assert) Standardı
Okunabilir ve bakımı kolay her birim testi (Unit Test), evrensel AAA (Arrange - Act - Assert) veya BDD karşılığı olan Given - When - Then şablonunu takip eder:
- Arrange (Given - Hazırlık): Test edilecek nesne (System Under Test - SUT) ve ön koşullar (örneğin içinde 2 ürün bulunan bir alışveriş sepeti ve %15 indirim kuponu) hazırlanır.
- Act (When - Eylem): Test edilmek istenen hedef metot çağrılır (
const total = cart.applyCoupon("BAHAR15")). - Assert (Then - Doğrulama): Dönen sonucun beklenen iş kuralıyla birebir örtüşüp örtüşmediği doğrulanır (
expect(total).toBe(850.00)).
2. Yazılım Test Piramidi (Testing Pyramid): Doğru Test Dengesini Kurmak
Mike Cohn tarafından ortaya konan Yazılım Test Piramidi (ve Guillermo Rauch / Kent C. Dodds tarafından modern ön yüz uygulamaları için uyarlanan Testing Trophy modeli), bir projede hangi test türünden ne oranda bulunması gerektiğini maliyet ve hız ekseninde açıklar. Piramidin tabanından tepesine doğru çıkıldıkça testlerin gerçekçiliği (entegrasyon düzeyi) artar, ancak çalışma hızı düşer ve bakım maliyeti katlanarak yükselir:
2.1. Taban Katman: Unit Tests (Birim Testleri — ~%70)
Piramidin en geniş tabanını oluşturur. Tek bir sınıfı, fonksiyonu veya Domain Aggregate nesnesini veritabanı, dosya sistemi ve ağdan tamamen izole biçimde doğrudan RAM üzerinde test eder. JUnit (Java), xUnit/NUnit (.NET), Vitest/Jest (TypeScript/JS) veya PyTest (Python) gibi araçlarla çalışır. İyi tasarlanmış bir projede 3.000 adet birim testi 5 saniyenin altında tamamlanmalıdır! Birim testlerinde dış bağımlılıkları izole etmek için Test Doubles (Mock, Stub, Spy, Fake) kullanılır. Ancak aşırı mock kullanımı (gerçekte var olmayan davranışları simüle etmek) testleri kırılganlaştırır; bu nedenle yalnızca dış altyapı sınırları (Port'lar) mock'lanmalı, saf Domain nesneleri asla mock'lanmamalıdır.
2.2. Orta Katman: Integration & Component Tests (Entegrasyon Testleri — ~%20)
Birim testleriniz %100 başarılı olsa bile, yazdığınız SQL sorgusunda bir sözdizimi hatası varsa veya ORM (Hibernate/Prisma) tablo ilişkilerini yanlış eşleştiriyorsa sistem canlıda çöker. Entegrasyon Testleri, kodunuzun gerçek veritabanı, gerçek Redis önbelleği ve gerçek mesaj kuyruğu (Kafka/RabbitMQ) ile doğru konuşup konuşmadığını doğrular.
Geçmişte entegrasyon testleri için sahte bellek içi veritabanları (H2, SQLite In-Memory) kullanılırdı; ancak PostgreSQL'e özgü bir JSONB sorgusu SQLite üzerinde çalışmadığı için bu yöntem terk edilmiştir. Günümüzde Testcontainers kütüphanesi endüstri standardıdır: Test başladığında Docker üzerinden 2 saniye içinde gerçek bir PostgreSQL, Redis veya Kafka konteyneri ayağa kaldırılır, göç (migration) dosyaları çalıştırılır, testler gerçek veritabanı üzerinde koşar ve test bittiğinde konteyner otomatik olarak silinir.
2.3. Tepe Katman: End-to-End (E2E - Uçtan Uca) Testler (~%10)
Piramidin en tepesinde yer alır. Gerçek bir web tarayıcısını (Chromium, Firefox, WebKit) veya mobil simülatörü ayağa kaldırarak, gerçek bir kullanıcının yaptığı gibi butonlara tıklar, formları doldurur, arka uç API'yi ve veritabanını tetikler ve ekranda "Siparişiniz Onaylandı" mesajının çıktığını doğrular. En yüksek güveni sağlar ancak çalıştırılması yavaştır (dakikalar sürer) ve arayüz değişikliklerine karşı hassastır. Bu nedenle yalnızca sistemin en kritik ticari akışları (User Journeys: Kayıt Olma, Giriş Yapma, Sepete Ekleme, Ödeme Yapma) E2E testleriyle korunmalıdır.
Tersine dönmüş bir piramide (Ice Cream Cone Anti-Pattern / Dondurma Külahı — çok az birim testi, yüzlerce yavaş E2E ve manuel test) sahip ekiplerde CI/CD boru hattı saatler sürer ve sürekli yanlış alarmlar (Flaky Tests) üretir.
3. Modern E2E Otomasyonu: Playwright vs. Cypress vs. Selenium
Uçtan uca (E2E) web otomasyonunda 15 yıl boyunca standart olan Selenium WebDriver, tarayıcı ile HTTP üzerinden harici sürücülerle haberleştiği için yavaşlık ve senkronizasyon hataları (Flaky Tests - bazen geçen bazen kalan testler) ile anılıyordu. Günümüzde modern web mimarilerinde yarış Microsoft Playwright ve Cypress arasındadır:
- Playwright (Endüstri Lideri): Doğrudan tarayıcının DevTools protokolüne kalıcı bir WebSocket bağlantısıyla bağlanır. Her tıklama öncesinde elemanın DOM'da görünür, tıklanabilir ve animasyonunun bitmiş olmasını otomatik bekler (Auto-Waiting). Her test için milisaniyeler içinde izole bir Browser Context açar, çoklu sekme (multi-tab) ve iframe senaryolarını doğal destekler ve hata anında zaman yolculuğu yapılabilen Trace Viewer ekran kaydı üretir.
- Cypress: Doğrudan tarayıcının kendi çalışma döngüsü (run-loop) içinde çalışır ve ön yüz geliştiricileri için mükemmel bir görsel hata ayıklama deneyimi sunar. Tek sayfa uygulamaları (SPA - React/Vue) ve bileşen (Component) testlerinde oldukça popülerdir.
| Karşılaştırma Kriteri | Unit Test (Birim Testi) | Integration Test (Testcontainers) | Contract Test (Pact) | E2E Test (Playwright / Cypress) |
|---|---|---|---|---|
| Test Kapsamı | Tek bir sınıf, fonksiyon veya Domain kümesi | Servis + Gerçek Veritabanı / Redis / Kafka | İki mikroservis arasındaki API/Olay şeması | Tüm sistem (UI + API + DB + 3. Parti) |
| Çalışma Hızı | Ultra Hızlı (1-10 milisaniye / test) | Orta-Hızlı (50-500 milisaniye / test) | Çok Hızlı (10-50 milisaniye / test) | Yavaş (2-15 saniye / senaryo) |
| Kırılganlık (Flakiness) | Sıfır (Deterministik ve izole) | Çok Düşük (İzole Docker konteynerleri) | Çok Düşük | Orta-Yüksek (Ağ, DOM ve zamanlama duyarlı) |
| Hata Noktasını Bulma | Nokta Atışı (Hangi satırın bozulduğu bellidir) | Yüksek (SQL/ORM veya adaptör katmanı) | Yüksek (Kırılan JSON/Protobuf alanı) | Genel (Hatanın hangi serviste olduğu araştırılır) |
| CI/CD Çalışma Aşaması | Her git commit ve PR anında |
Her Pull Request (PR) birleştirmesinde | Her PR ve servis dağıtımı öncesinde | Ana dala (Main) geçişte ve Staging ortamında |
4. Mikroservislerde Kontrat Testleri (Consumer-Driven Contract Testing - Pact)
Bir Composable Commerce veya mikroservis mimarisinde 30 farklı servisin birbiriyle konuştuğunu düşünün. Sipariş Servisi, Katalog Servisinden ürün bilgisini çekerken JSON içinde priceInCents alanını beklemektedir. Katalog ekibi bir gün kendi servisinde bu alanın adını unitPrice olarak değiştirir; Katalog ekibinin kendi birim testleri %100 yeşil geçer ama kod canlıya çıktığı anda Sipariş Servisi çöker! 30 mikroservisin tamamını aynı test ortamında ayağa kaldırıp E2E testi yapmak ise hem çok pahalı hem de çok yavaştır.
Bu problemi çözen modern yaklaşım Consumer-Driven Contract Testing (Tüketici Güdümlü Kontrat Testi - Pact) tekniğidir: İstemci servis (Consumer), karşı servisten beklediği HTTP/Mesaj şemasını bir "Kontrat Dosyası (Pact JSON)" olarak üretir ve merkezi Pact Broker'a yükler. Sağlayıcı servis (Provider) kendi CI/CD boru hattında her derleme yaptığında bu kontratı indirir ve "Benim ürettiğim yanıt, müşterilerimin beklediği şemayı bozuyor mu?" testini saniyeler içinde bağımsız olarak çalıştırır. Benzer şekilde yapay zeka destekli tersine mühendislik süreçleriyle eski sistemler yenilenirken, eski API ile yeni API'nin birebir aynı çıktıyı verdiğini doğrulamak için Kontrat Testleri ve Golden Master (Snapshot) testleri hayati rol oynar.
5. %100 Kod Kapsama Oranı (Code Coverage) Yanılsaması ve Mutation Testing
Birçok kurumsal şirkette yönetim tarafından "Projelerde en az %80 veya %100 Code Coverage (Kod Kapsama Oranı) zorunludur" kuralı konur. Ancak yüksek Code Coverage, kaliteli test anlamına gelmez! Code Coverage araçları (JaCoCo, Istanbul/c8) yalnızca bir kod satırının test sırasında çalıştırılıp çalıştırılmadığını ölçer; sonucun doğrulanıp doğrulanmadığını (Assertion kalitesini) ölçemez. İçinde hiçbir expect() veya assert() satırı bulunmayan sahte testler yazarak %100 Code Coverage elde edebilirsiniz ama bu testler hiçbir hatayı yakalayamaz.
Testlerinizin gerçekten işe yarayıp yaramadığını ölçen asıl yöntem Mutation Testing (Mutasyon Testi - Stryker, PITest) tekniğidir: Mutasyon test aracı, sizin üretim kodunuzun içine gizlice küçük hatalar (Mutant'lar) yerleştirir; örneğin kodunuzdaki if (age >= 18) operatörünü if (age > 18) veya + işaretini - olarak değiştirir ve testlerinizi tekrar çalıştırır. Eğer kodunuz bozulduğu halde testleriniz hala yeşil yanıyorsa (Mutant Survived), testleriniz sınır değerleri (boundary conditions) kontrol etmiyor demektir! Eğer testleriniz kırmızı yanarak hatayı yakalarsa (Mutant Killed), test kaliteniz kanıtlanmış olur.
6. Fonksiyonel Olmayan Testler: Yük, Stres (k6) ve Güvenlik Testleri
Bir yazılımın iş kurallarının doğru çalışması (Fonksiyonel Testler), o yazılımın canlı ortamda ayakta kalacağı anlamına gelmez. Sistemin performans, ölçeklenebilirlik ve güvenlik sınırlarını doğrulamak için CI/CD hattına şu testler de eklenmelidir:
- Yük ve Stres Testleri (Load & Stress Testing — Grafana k6, Locust, Gatling): Kodunuz tek kullanıcıyla 20 ms'de çalışırken, aynı anda 25.000 eşzamanlı kullanıcı (Virtual Users) yüklendiğinde veritabanı bağlantı havuzu (Connection Pool) tükeniyor mu, bellek sızıntısı (Memory Leak) oluyor mu ve p95/p99 yanıt süreleri SLA sınırları içinde kalıyor mu? Grafana k6 ile JavaScript kullanarak kod gibi versiyonlanabilen yük testleri yazılır.
- Akıllı Kontrat ve Güvenlik Testleri (SAST / DAST / Fuzzing): Özellikle Web3 ve merkezsiz uygulamalarda dağıtılan kod sonradan değiştirilemediği için Foundry/Hardhat ile rastgele binlerce uç girdi üreten Property-Based / Fuzz Testing uygulanır. Aynı şekilde kurumsal No-Code B2B bayi portallerinde de API uç noktaları otomatik güvenlik taramaları (OWASP ZAP) ve duman testleriyle (Smoke Tests) sürekli denetlenir.
Sıkça Sorulan Sorular (SSS)
1. TDD (Test-Driven Development) ile kod yazmak projeyi yavaşlatır mı?
İlk birkaç haftada özelliğin kodlanma süresini %15-20 oranında uzatıyormuş gibi görünse de, projenin toplam yaşam döngüsünde geliştirme hızını 2 katından fazla artırır. Çünkü TDD ile yazılan kodda hata ayıklama (debugging) konsolunda saatlerce vakit kaybetmezsiniz ve 6 ay sonra koda yeni bir özellik eklerken eski özellikleri bozma (regresyon) maliyeti sıfıra iner.
2. Birim Testi (Unit Test) ile Entegrasyon Testi arasındaki sınır tam olarak neresidir?
Eğer yazdığınız test çalışırken ağ soketi açıyor, gerçek bir veritabanına bağlanıyor, diskteki bir dosyayı okuyor veya işletim sistemi zamanlayıcısını bekliyorsa o bir Entegrasyon Testidir. Eğer tamamen bellek (RAM) içinde, dış dünyadan %100 izole ve milisaniyeler içinde deterministik olarak çalışıyorsa o bir Birim Testidir.
3. TDD vs. BDD (Behavior-Driven Development) arasındaki fark nedir?
TDD geliştirici odaklıdır ve doğrudan programlama diliyle birim/tasarım doğruluğunu hedefler. BDD (Davranış Güdümlü Geliştirme - Cucumber / Gherkin) ise iş analistleri, ürün sahipleri ve yazılımcıların ortak anlayabileceği doğal dilde (Given kullanıcı sepetteyken / When ödeme butonuna bastığında / Then sipariş onaylanmalı) senaryolar yazarak iş beklentilerini otomatik kabul testlerine dönüştürür.
4. Mevcut (Legacy) ve hiç testi olmayan eski bir projeye nasıl test eklenir?
Hiç testi olmayan devasa bir monolitik koda doğrudan birim testi yazamazsınız çünkü sınıflar birbirine sıkı sıkıya bağlıdır. Michael Feathers'ın "Working Effectively with Legacy Code" kitabında belirttiği üzere; önce değiştirilecek bölgenin etrafına mevcut davranışı kaydeden yüksek seviyeli Karakterizasyon Testleri (Golden Master / E2E / API Testi) yazılır, ardından bu koruma altında bağımlılıklar kırılarak adım adım birim testlerine geçilir.
5. Flaky Test (Kararsız Test) nedir ve nasıl önlenir?
Kodda hiçbir değişiklik yapılmadığı halde bazen başarılı (Yeşil) bazen başarısız (Kırmızı) olan güvenilmez testlere Flaky Test denir. En yaygın nedenleri; testlerin içine sabit bekleme süreleri (sleep(3000)) koymak, testlerin aynı veritabanı satırlarını birbirini ezecek şekilde paylaşması ve rastgele tarih/saat kullanılmasıdır. Çözüm; her testi izole verilerle çalıştırmak, sistem saatini soyutlamak (Clock abstraction) ve Playwright gibi olay bazlı Auto-Waiting mekanizmaları kullanmaktır.
Yozel
Codex haberleri en az iki bağımsız kaynakla doğrulanır; düzeltmeler yazının altında tarih ve saatle belirtilir. Yayın ilkelerimiz