Buğra Şıkel

Mikroservis mimarisiyle tasarlanmış bir finansal mutabakat (reconciliation) sisteminde karşılaştığımız kritik bir vakayı ele alalım. SonarQube kalite kapılarımız (quality gates), ilgili domain modülü için Branch Coverage oranını %96.4 olarak raporluyordu. CI/CD pipeline’ındaki tüm birim testler yeşil yanıyor, statik kod analizi sıfır zafiyet gösteriyordu. Ancak, ay sonu mutabakatında, döviz kuru dönüşümlerindeki bir sınır değer (boundary value) hatası nedeniyle 1.2 milyon dolarlık bir işlem hacminin %0.04’lük kısmında kuruş hassasiyeti (precision) sapması yaşandı.
Buradaki mimari yanılgı, kodun çalıştırılmış olmasını (execution), kodun doğrulanmış olmasıyla (assertion) karıştırmaktır. Bir if-else bloğunun tüm dallarından test koşumu sırasında geçilmesi, o dallardaki iş mantığının doğru test edildiği anlamına gelmez. Sadece o satırların JVM veya V8 motoru üzerinde işletildiğini matematiksel olarak kanıtlar. 15 yıllık production tecrübem bana şunu net bir şekilde öğretti: Test süitinizdeki asıl tehlike test edilmemiş kodlar değil, test edildiği sanılan kodlardır.
İşte bu illüzyonu yıkmak ve birim testlerdeki kör noktaları (blind spots) tespit etmek için, kodu kasıtlı olarak sabote eden Mutation Testing stratejisini devreye almamız gerekir. Bu karar matrisi, geleneksel kapsama metrikleri ile mutasyon tabanlı analiz arasındaki yapısal farkları, trade-off’ları ve production ortamı için entegrasyon stratejilerini incelemektedir.
Branch coverage (dal kapsamı), kontrol akış grafiğindeki (control flow graph) karar noktalarının (if, switch, while, for) test koşumu sırasında hangi oranda değerlendirildiğini ölçer. Matematiksel temeli, McCabe’in Siklomatik Karmaşıklık (Cyclomatic Complexity) algoritmasına dayanır.
Bu metrik, kodun üzerinden geçilip geçilmediğini byte-code veya kaynak kod enstrümantasyonu (instrumentation) ile takip eder. Java ekosisteminde JaCoCo, JavaScript/TypeScript ekosisteminde Istanbul (nyc) bu işi AST (Abstract Syntax Tree) seviyesine sayaçlar yerleştirerek yapar.
Aşağıdaki Java kod bloğunu inceleyelim. Geleneksel metriklerin bizi nasıl yanılttığını açıkça görebiliriz:
public class LoyaltyCalculator {
public BigDecimal calculateDiscount(BigDecimal cartTotal, int customerTenureYears) {
// Sınır değer kontrolü
if (cartTotal.compareTo(new BigDecimal("1000")) > 0 && customerTenureYears >= 5) {
return cartTotal.multiply(new BigDecimal("0.85")); // %15 indirim
}
return cartTotal;
}
}
// JUnit 5 Test Sınıfı
class LoyaltyCalculatorTest {
@Test
void testCalculateDiscount() {
LoyaltyCalculator calculator = new LoyaltyCalculator();
// Dal 1: İf koşulu sağlanır (TRUE)
calculator.calculateDiscount(new BigDecimal("1500"), 6);
// Dal 2: İf koşulu sağlanmaz (FALSE)
calculator.calculateDiscount(new BigDecimal("500"), 2);
// DİKKAT: Hiçbir assertion (doğrulama) yapılmadı!
// Veya yetersiz bir assertion yapıldı: assertNotNull(calculator);
}
}
Yukarıdaki test çalıştırıldığında JaCoCo raporu %100 Branch Coverage gösterecektir. Çünkü if bloğunun hem true hem de false yolları çalıştırılmıştır. Ancak test, metodun döndürdüğü indirimli tutarın doğruluğunu asla kontrol etmez. Bu duruma literatürde Assertion-Free Testing veya Assertion Roulette adı verilir. Eğer bir geliştirici 0.85 çarpanını yanlışlıkla 0.95 olarak değiştirirse, test yine başarıyla geçecek ancak business logic production’da çökecektir.
Mutation testing, “Testleriniz kodunuzu test ediyor, peki testlerinizi kim test ediyor?” sorusuna verilen donanımsal ve algoritmik bir cevaptır. Mantık basittir: Orijinal kaynak kodun belirli kısımlarını (operatörler, koşullar, dönüş değerleri) kasıtlı olarak bozarız (mutasyon). Oluşan bu yeni hatalı koda Mutant denir. Ardından mevcut test süitini bu mutant kod üzerinde çalıştırırız.
Java ekosistemindeki endüstri standardı olan PiTest (PIT), kaynak kodu yeniden derlemek yerine, bellekteki (in-memory) JVM bytecode’unu ASM kütüphanesi yardımıyla manipüle eder. Bu sayede I/O maliyetinden kaçınır. Uygulanan temel mutasyon operatörleri şunlardır:
< operatörünü <= yapar. Sınır değer (off-by-one) hatalarını bulmak için kritiktir.+) işlemini çıkarma (-) ile değiştirir.eventPublisher.publish(event)) koddan tamamen siler.true dönen bir metodu false dönecek veya null referans dönecek şekilde değiştirir.Mutation Testing, kodu değil, testin kalitesini metrikleştirir. Hedefi test sayısını artırmak değil, mevcut testlerin fault-detection (hata yakalama) kapasitesini maksimize etmektir.
Mimari kararlar alırken, iki metrik arasındaki yapısal farklılıkları anlamak CI/CD süreçlerinin sağlığı için elzemdir.
| Özellik / Kriter | Branch Coverage (JaCoCo, Istanbul) | Mutation Testing (PiTest, Stryker) |
|---|---|---|
| Ölçüm Odak Noktası | Kod satırlarının işletilme (execution) oranı | Testlerin hata yakalama (assertion) kalitesi |
| İşlem Süresi (CPU Cost) | Milisaniyeler/Saniyeler seviyesinde (Düşük) | Dakikalar/Saatler seviyesinde (Çok Yüksek) |
| CI/CD Entegrasyonu | Her pull request’te (PR) senkron olarak koşulabilir | Gece koşumları (Nightly) veya sadece değişen dosyalar bazında (Incremental) koşulmalıdır |
| Yanlış Pozitif (False Positive) | Yok denecek kadar az | Yüksek (Equivalent Mutants – Eşdeğer Mutantlar sorunu) |
| Altyapı İhtiyacı | Standart CI runner’ları yeterlidir (1-2 CPU) | Yüksek paralelizasyon gerektirir (Çok çekirdekli runner, örn: 8-16 vCPU) |
| Kör Nokta Tespiti | Sınır değer hatalarını (boundary) kaçırır | Off-by-one ve eksik assertion tespitinde %98+ başarı |
Yazılım mimarisinde gümüş kurşun (silver bullet) yoktur. Trade-off analizi yaparken bağlamı (context) değerlendirmek zorundayız.
2023’ün 3. çeyreğinde, saniyede 400 işlem (TPS) kapasiteli bir sepet (cart) hesaplama mikroservisini yeniden faktör ediyorduk (refactoring). TypeScript tabanlı bu projenin Jest ile çalıştırılan branch coverage oranı %98.2 idi. Projeye Stryker Mutator entegre etmeye karar verdik.
İlk koşum sonuçları sarsıcıydı:
Hayatta kalan mutantları incelediğimizde, kupon kullanım süresini doğrulayan şu satırda bir kör nokta tespit ettik:
// Orijinal Kod
if (coupon.expirationDate.getTime() >= currentRequestDate.getTime()) {
applyDiscount();
}
Stryker, bu koddaki >= operatörünü > olarak (ConditionalsBoundary) mutasyona uğratmıştı. Ancak ilgili unit testlerde tam olarak son kullanma milisaniyesine denk gelen bir mock veri yazılmadığı için test yeşil yanmaya devam ediyordu. Eğer production’da tam sınır sürede gelen bir istek olsaydı, kupon geçerli olmasına rağmen reddedilecekti. Stryker bu eksik testi bize gösterdi.
Performans İyileştirmesi (Optimizasyon): Stryker’ın ilk koşumu CI pipeline’ında 22 dakika sürdü (Önceki Jest süresi 45 saniyeydi). CI süreçlerini tıkamamak için şu konfigürasyonları uyguladık:
// stryker.conf.json
{
"mutate": [
"src/domain/**/*.ts", // Sadece core domain mantığını hedefe aldık
"!src/infrastructure/**/*.ts"
],
"incremental": true, // Cache mekanizmasını aktif ettik
"concurrency": 6, // CI runner'in 8 çekirdeğinden 6'sını Stryker'a atadık
"coverageAnalysis": "perTest"
}
Bu konfigürasyonla mutation testing süresi PR bazında 22 dakikadan p99 2 dakika 15 saniyeye düştü. CPU kullanımında gereksiz yük engellendi.
Mimari bir standardizasyon oluştururken dikkat etmeniz gereken checklist:
@Entity, @DomainService veya /domain klasörleriyle sınırlayın.Kesinlikle hayır. Branch coverage bir kod dalından geçildiğini garanti eder. Condition/Edge coverage tüm lojik kombinasyonların (A && B) denendiğini gösterir. Ancak hiçbiri testinizin assertEquals veya expect fonksiyonlarıyla dönüş verisini doğru bir şekilde kontrol ettiğini garanti etmez. Mutation testing ise assert mantığının kalitesini ölçer.
Bu beklenen bir durumdur. 100 sınıflık bir projede binlerce mutant oluşabilir ve test süitiniz binlerce kez baştan çalıştırılır. Çözüm: Testlerinizi CoverageAnalysis: perTest modunda yapılandırın. Bu sayede araç, sadece mutasyona uğrayan satırı test eden spesifik testleri (tüm süiti değil) çalıştırır. İşlem süresinde p99 oranında düşüş yaşarsınız.
Kod sabote edilse bile, yazılımın dışarıdan gözlemlenebilir davranışının değişmediği durumlardır. Örneğin: int a = 5; int b = a * 1; kodunu int b = a / 1; olarak mutasyona uğratmak iş mantığını değiştirmez. Bu mutantı testin yakalayamaması normaldir ve yanlış pozitif olarak rapora yansır. Geliştiricinin bu mutantları rapor üzerinde manuel olarak “ignore” (göz ardı) etmesi gerekebilir.
Hayır. Mutation testing, yüksek hızlı, dış bağımlılığı mock’lanmış Unit (Birim) testler için tasarlanmıştır. Veritabanına bağlanan bir integration testi milisaniyeler değil saniyeler sürer. Bunu yüzlerce mutant ile çarparsanız pipeline süresi günler sürer. Kesinlikle Anti-Pattern’dir.
Branch coverage, projenizin test altyapısının temel hijyen standardıdır; size test edilmemiş alanların haritasını verir. Ancak testlerinizin iş mantığını gerçekten doğrulayıp doğrulamadığını kanıtlayamaz. Güvenlik veya finansal tutarlılık gerektiren kritik domain modüllerinizde, testlerinizi mutation testing süzgecinden geçirmedikçe gerçek bir kalite güvencesinden (quality assurance) söz edemezsiniz.
Action item olarak: Pazartesi günü en kritik mikroservisinizin sadece domain katmanına PiTest (Java), Stryker (JS/TS) veya Infection (PHP) entegre edin ve lokal bilgisayarınızda çalıştırın. Ortaya çıkan survived mutantların loglarını incelediğinizde, sisteminizde sessizce bekleyen potansiyel production kesintilerini kendi gözlerinizle göreceksiniz.

Bu blog yazısı, mevcut yüksek performanslı C++ görüntü işleme boru hatlarına Python tabanlı yapay zeka modellerini entegre etmek için sıfır kopyalama yaklaşımlarını inceler. Veri kopyalama maliyetlerini ortadan kaldırarak performansı artırma ve bellek kullanımını optimize etme yöntemlerini detaylandırır.
Devamını Oku

React Native Bridge’in 2-5ms’lik gecikme bariyerini aşın. C++ Host Object deseniyle 15 mikrosaniye tepki süreli, senkron ve belleği optimize eden JSI modüllerini production ortamı için sıfırdan inşa edin.
Devamını Oku

Production ortamında veri kaybı yaratan UPDATE komutlarından kurtulup, p99 gecikmesini 400ms’den 18ms’ye düşüren Event Sourcing mimarisine aşamalı geçiş.
Devamını Oku