İçeriğe geç
Hüseyin Bozdemir
Tüm yazılar

AI'ın yazdığı testler hatayı yakalar mı?

AI'a yazdırılan unit testlerde kapsam ve mutation skoru %100 çıkıp hata yine kaçabilir. C# ve Stryker.NET ile dört varyantlı bir ölçüm.

8 dk okumaGüncellendi:

'Her ölçü tam. Hata yerinde.' başlığı; sağda tikli üç satır: Testler 42/42 yeşil, Dal kapsamı %100, Mutation skoru %100; altında turuncu çerçeveli kod kutusu: if (araToplam > 250), olması gereken >= 250, 250 TL'lik sepette kupon çalışmıyor.
İçindekiler
  1. Kapsam neyi ölçer, mutation testing neyi?
  2. Deney
  3. Beklenti tutmadı: %100
  4. Hatayı koyunca: yeşil testler, %100 skor
  5. Kuralı verince test kırıldı
  6. Neden böyle oluyor?
  7. Eski kodu bilerek mühürlemek
  8. AI’a test yazdırırken
  9. Ölçmediğim şeyler
  10. Sık sorulanlar
  11. Mutation skoru %100 ise testler iyi mi demektir?
  12. AI’a test yazdırırken iş kuralı nasıl verilmeli?
  13. Characterization test nedir, ne zaman kullanılır?

Bir faturanın durumunu bankadan gelen koda göre eşleyen eski bir kod vardı. Kural kısaydı: bankadan “A” gelirse faturayı ödendi olarak işaretle. Yapay zekâya bu koda test yazdırdım; testler kurala uygun, düzgün ve yeşildi. Sonra bankada “A”nın bambaşka bir durum olduğu ortaya çıktı. Testler hatayı yakalamamıştı, üstüne bir de “doğru” diye imzalamıştı.

Kısa cevap: test kalitesini ölçen iki sayıda da, kapsamda (coverage) ve mutation skorunda (koda bilerek konan küçük hataların testler tarafından yakalanma oranı), AI’ın testleri doğru kodda tam not aldı. Koda bilerek bir hata koyduğumda da tam not aldı. Testler yeşildi, kapsam %100’dü, mutation skoru %100’dü ve hata oradaydı.

Yeni bir kodu iş kuralıyla birlikte verdiğimde AI temiz test yazıyor. Eski kodda ise kural hatalıysa test de hatalı geliyor. Bu farkın ne kadar büyük olduğunu C# ve Stryker.NET ile dört varyantta ölçtüm.

Kapsam neyi ölçer, mutation testing neyi?

Kapsam, testler çalışırken kodun hangi satırlarından ve dallarından geçildiğini sayar. Bir if’in iki kolu da çalıştıysa dal kapsamı tamdır. Ama kapsam, testin sonucu doğru kontrol edip etmediğini bilmez; hiçbir Assert içermeyen bir test de satırları “kapsar”.

Mutation testing bu boşluğu kapatmak için var. Araç, kodda küçük bir değişiklik yapar (buna mutant denir), sonra testleri çalıştırır. Örneğin:

if (araToplam >= SabitKuponEsigi)   // asıl kod
if (araToplam > SabitKuponEsigi)    // mutant

Testlerden en az biri kırılırsa mutant “öldürülmüş” sayılır: testler bu değişikliği fark etmiştir. Hepsi yeşil kalırsa mutant “hayatta kalır” ve testlerinde bir kör nokta olduğunu gösterir. Mutation skoru, öldürülen mutantların oranıdır. .NET’te bunun aracı Stryker.NET; Microsoft’un dokümantasyonunda da kısa bir rehberi var.

Yaygın kanı şu: AI’ın yazdığı testler kapsamı kolayca şişirir, mutation skoru ise bunun içinin boş olduğunu ortaya çıkarır. Ben de bunu bekliyordum.

Deney

Sıfırdan küçük bir sepet hesaplayıcı yazdım. Kuralı az ama sınır değeri çok; mutation testing’in en iyi yakaladığı türden:

  • Ara toplam 1.000 TL ve üstündeyse %5 kademe indirimi.
  • YUZDE10 kuponu kalan tutarın %10’u, en fazla 200 TL.
  • SABIT50 kuponu 50 TL; ara toplam 250 TL ve üstündeyse geçerli.
  • İndirimlerden sonra 500 TL ve üstü ücretsiz kargo, altı 49,90 TL. Ağırlık 20 kg’ı aşarsa ücretsiz kargoda bile 30 TL ağır yük ücreti.
  • Her adım kuruşa yuvarlanır (MidpointRounding.AwayFromZero).

Testleri Claude Opus 5.5 yazdı. Her seferinde bağlamı boş yeni bir oturum açtım, yalnızca sınıfı verdim ve aynı cümleyi kullandım: “Bu C# sınıfı için xUnit testleri yaz.” Testlere elle dokunmadım. Ölçüm: coverlet ile kapsam, Stryker.NET 5.0 ile mutation skoru, .NET 10 üzerinde.

Aynı sınıfı dört şekilde verdim:

Varyant Kurallar sınıfın başındaki yorumda Kod
A var doğru
B yok doğru
C yok hatalı
D var hatalı

Hata tek karakterlik: SABIT50 kontrolünde >= yerine >. Yani tam 250 TL’lik sepette kupon sessizce çalışmıyor. Sınırda bir birimlik kayma (off-by-one); 250,00 TL’lik sepet nadir olduğu için uzun süre fark edilmeyebilir.

Beklenti tutmadı: %100

Doğru kodda iki varyantın sonucu:

Test sayısı Dal kapsamı Mutation skoru
A (kurallar yorumda) 56 %100 %100 (41/41)
B (yorum yok) 47 %100 %100 (41/41)

Hayatta kalan tek mutant yok. Testler eşiklerin hem hemen altını hem tam kendisini deniyordu: 999,99 ve 1.000; 249,99 ve 250; 499,99 ve 500; 20 kg ve 20,001 kg. Yarım kuruşu yukarı yuvarlamayı, kupondan sonra kargo kararının değiştiği durumu, ücretsiz kargoda ağır yük ücretini ayrı ayrı yakalıyordu. Çoğu test, sonucun tamamını tek seferde karşılaştırıyordu:

[Fact]
public void Sabit50_esikte_50_TL_duser()
{
    var sonuc = Hesapla(250m, "SABIT50");

    Assert.Equal(new SepetSonucu(250m, 0m, 50m, 49.90m, 249.90m, false), sonuc);
}

Bu, mutation testing açısından güçlü bir alışkanlık. Tek bir alanı kontrol eden test, diğer alanlardaki değişikliği kaçırır; record’un tamamını karşılaştıran test kaçırmaz.

B’nin sonucu beni A’dan daha çok şaşırttı. Kuralları anlatan yorumu kaldırınca da AI sınırları kodu okuyarak buldu. Yani bu modülde “AI’ın testleri yüzeysel kalır” iddiası tutmadı.

Bir yan not: coverlet’in raporunda satır kapsamı A için %79, C için %69 göründü. İkisinde de çalışan kodun tamamı kapsanıyordu. Açıkta kalan satırlar const bildirimleri ve record’un property’leriydi; testler Assert.Equal ile record’u bütün olarak karşılaştırınca getter’lar hiç çağrılmıyor. Aynı kapsamda iki test dosyası, yazım tarzı yüzünden on puan farklı sayı alabiliyor. Satır yüzdesini tek başına ölçü yapmamak için bir neden daha.

Hatayı koyunca: yeşil testler, %100 skor

C varyantında kod hatalı ve kurallar yorumda yok. AI 42 test yazdı, hepsi geçti. Dal kapsamı %100, mutation skoru %100.

AI 250 TL sınırını gördü ve test etti. Ama gördüğü davranışı doğru kabul etti:

[Fact]
public void Sabit50_esikte_uygulanmaz_ama_gecersiz_sayilmaz()
{
    Assert.Equal(Beklenen(250m, 0m, 0m, 49.90m, 299.90m), H("SABIT50", K(250m)));
}

Testin adı durumu özetliyor: “Eşikte uygulanmaz.” Kuralı bilmeyen biri için bu, kodun söylediğinin dürüst bir tarifi. Kuralı bilen biri için bir hata raporu.

Asıl ilginç olan, mutation testing’in burada ne yaptığı. Stryker’ın denediği mutantlardan biri >’yi >=’ye çevirmek, yani hatayı düzeltmek. Bu test o mutantı öldürüyor: biri hatayı düzeltirse test kırılıyor. Mutation testing, hatalı davranışı korumaya yardım etti. Çünkü ölçtüğü şey testlerin değişikliği fark edip etmediği; değişikliğin iyi mi kötü mü olduğunu bilemez.

Kuralı verince test kırıldı

D varyantında kod aynı şekilde hatalı ama sınıfın başında kurallar yazıyor: “ara toplam 250 TL ve üstündeyse geçerli”. Bu sefer AI kuralın tarafını tuttu:

[Fact]
public void Sabit50_tam_esikte_250_TL_gecerlidir()
{
    // Kural: "ara toplam 250 TL ve üstündeyse geçerli".
    var sonuc = Hesapla(250m, "SABIT50");
    Assert.Equal(new SepetSonucu(250m, 0m, 50m, 49.90m, 249.90m, false), sonuc);
}
Expected: SepetSonucu { AraToplam = 250, KuponIndirimi = 50, Odenecek = 249,90, ... }
Actual:   SepetSonucu { AraToplam = 250, KuponIndirimi = 0,  Odenecek = 299,90, ... }

44 testten biri kırıldı ve kırılan test tam olarak hatayı gösteriyordu. Aynı model, aynı istem, aynı kod; tek fark, kuralın koddan ayrı bir yerde yazılı olması.

Neden böyle oluyor?

Test literatüründe bunun bir adı var: oracle problemi. Bir testin iki parçası vardır: girdiyi hazırlayan kısım ve “doğru sonuç şu olmalı” diyen kısım. İkinci kısım, yani oracle, beklenen davranışı bilmeyi gerektirir. Kodu okuyarak bulabileceğin şey ise kodun bugünkü davranışıdır.

AI’a yalnızca kodu verdiğinde, beklenen davranış için elindeki tek kaynak o kod. 24 Java projesi üzerinde yapılan bir çalışma da aynı sonuca varıyor: LLM ile üretilen testler çoğunlukla programın beklenen değil gerçek davranışını yakalıyor, bu da hata bulmayı zorlaştırıyor. Benim küçük deneyim bu bulgunun tek bir örneği.

Fatura hikâyesine dönünce bir ayrıntı daha var. Orada hata koddan değil kuralın kendisinden geliyordu: “A gelirse ödendi” kuralı da yanlıştı. Testleri o kuralla yazdırsaydım yine yeşil kalırlardı. Yani “kuralı ver” derken kastettiğim, kuralın asıl kaynağı: bankanın durum kodu tablosu, sözleşme, analiz dokümanı. Kodun başındaki yorum bile çoğu zaman kodu yazanın anladığıdır.

Eski kodu bilerek mühürlemek

Burada bir ayrım yapmak gerekiyor, çünkü “bugünkü davranışı kilitleyen test” her zaman kötü değil.

Eski bir koda dokunmadan önce, onun şu an ne yaptığını testlerle kilitlemek bilinen bir yöntem: characterization test. Amacı, refactor sırasında davranışın değişmediğinden emin olmak. Bu iş için AI gerçekten iyi; C varyantındaki testler refactor güvencesi olarak çok işe yarar.

Sorun, bu testlerin sonradan “kodun doğru olduğunun kanıtı” diye okunması. Altı ay sonra biri 250 TL’lik sepette kuponun çalışmadığını bildirdiğinde, ilk tepki kolayca “testi var, öyle olmalı” olabilir.

AI’a test yazdırırken

Bu ölçümden ve kendi yaşadığımdan çıkardıklarım:

  • Kuralı koddan ayrı ver. Yeni kodda zaten iş kuralıyla birlikte veriyorsun, sorun çıkmıyor. Eski kodda da kuralı ayrıca yaz ya da kaynağını ekle; yalnızca sınıfı verirsen test, kodun fotokopisi olur.
  • Kırılan testi hemen “düzelt” deme. D’de kırılan test, hatayı bulan testti. AI’a “testler kırıldı, düzelt” dersen büyük ihtimalle testi koda uydurur ve C’ye dönersin. Önce kodun mu testin mi yanlış olduğuna karar ver.
  • Eski koda yazdırılan testleri ne olduklarıyla adlandır. Bunlar characterization test. İsimde ya da açıklamada “mevcut davranış” dediğinde, okuyan kişi onları şartname sanmaz.
  • Mutation skorunu doğru oku. Yüksek skor, testlerin değişikliği fark ettiğini söyler. Testlerin doğru şeyi koruduğunu söylemez. Hatalı davranış da %100 skorla korunabiliyor.

Ölçmediğim şeyler

Bu deney küçük: 80 satırlık tek bir modül, tek bir model, her varyant için tek bir üretim. Başka araçları (Copilot, Cursor vb.) denemedim. Yan etkisi olan, veritabanına ya da dış servise bağlı büyük bir sınıfta AI’ın sınırları bu kadar iyi bulup bulmadığını da bilmiyorum; benim modülümdeki sınırlar kodda açık açık sabit olarak duruyordu. Gerçek kodda kural çoğu zaman üç sınıfa dağılmış durumda ve oraya baktığımda sonucun bu kadar temiz çıkacağını sanmıyorum.

Kesin söyleyebileceğim tek şey şu: kodu verdiğinde AI kodun ne yaptığını test eder; ne yapması gerektiğini test etmesini istiyorsan, bunu ona sen söylemelisin.

Sık sorulanlar

Mutation skoru %100 ise testler iyi mi demektir?

Yüksek skor, testlerin koddaki değişikliği fark ettiğini söyler; doğru şeyi koruduğunu söylemez. Ölçümde hatalı kod da %100 skor aldı, üstelik Stryker’ın hatayı düzelten mutantını test öldürdü: mutation testing hatalı davranışı korumaya yardım etti.

AI’a test yazdırırken iş kuralı nasıl verilmeli?

Koddan ayrı ve asıl kaynağıyla: bankanın durum kodu tablosu, sözleşme, analiz dokümanı. Yalnızca sınıfı verirsen test kodun fotokopisi olur; D varyantında kuralı sınıfın başına yazınca aynı model aynı istemle hatayı bulan testi yazdı. Kodun başındaki yorum bile çoğu zaman kodu yazanın anladığıdır, kaynağın kendisi değil.

Characterization test nedir, ne zaman kullanılır?

Eski bir koda dokunmadan önce onun şu an ne yaptığını testlerle kilitlemektir; amacı refactor sırasında davranışın değişmediğinden emin olmak. AI bu iş için gerçekten iyi. Tehlike, bu testlerin sonradan “kodun doğru olduğunun kanıtı” diye okunması; isimde ya da açıklamada “mevcut davranış” dediğinde okuyan kişi onları şartname sanmaz.