İncelememe bir pull request düştü: diff’in yarısı testti ve onları bir model yazmıştı. Yerelde geçiyorlardı. CI’da biri bir kez patladı, yeniden çalıştırınca geçti, iki gün sonra yük altında yine patladı. Koda kimse dokunmamıştı.
O test yanlış değildi. Kararsızdı (flaky) — ve kararsızlık, kendine has bir bozukluk türü.
AI kararsızlığı icat etmedi — seri üretime geçirdi
Eskiden test yazmanın bir maliyeti vardı; bu yüzden daha az yazar, her birini daha çok düşünürdün. Model bunları saniyeler içinde, yalnızca kodun o anki görünüşünden yazıyor. Eşzamanlılığı, kaynak limitlerini, bir async çağrının gerçek zamanlamasını göremez — o yüzden doğru görünen ama sessizce deterministik olmayan kalıplara uzanır:
- Async işi “beklemek” için sabit bir sleep:
time.Sleep(2 * time.Second),await page.waitForTimeout(2000),cy.wait(2000). - Her çalıştırmada farklı değer üreten, seed’siz bir rastgele kaynak:
Math.random(),rand.Intn(100), sabit seed’i olmayan birfaker. - DOM yapısına çakılı, kırılgan bir selector —
nth-child, otomatik üretilmiş bir class adı — markup azıcık kayınca kırılır. - Her şeye bir mock; böylece test bir sonucu değil bir etkileşimi doğrular — hiçbir şey yapmayan testlerle coverage kovalamak yazısında anlattığım o boş yeşilin aynısı.
Tek tek bakınca her biri makul. Ölçekte bir kararsızlık fabrikası.
Süit büyüdükçe daha az güvenilir oluyor
%0,5 ihtimalle boşuna patlayan tek bir test görünmez. Aynısından 800 tane koy, temiz bir çalıştırmada herhangi bir testin patlama ihtimali 1 − 0,995^800 ≈ %98. Patlamalar emekten bağımsız da değil: CI, paylaşımlı ve throttle’lı donanımda koşar; laptop’unda 50 ms’de biten async test, yük altında timeout’a düşer.
Model test eklemeye devam ettikçe bu sayı tırmanır ve süitin yanlış-kırmızı verme ihtimali kesinliğe doğru yürür. Önemsediğin geçiş, önemsemediğin gürültüde boğulur.
Retry bir koşu bandı, tedavi değil
Refleks, gürültüyü yutmak: job’u otomatik retry’la, testi flaky işaretle, quarantine’e al, bir ticket aç. Bunların hepsi reaktif — test CI’ı zaten bozduktan, çoğu zaman birden çok kez bozduktan sonra devreye girer.
- Retry gerçek CI dakikası yakar ve feedback loop’u uzatır; yeni hiçbir şey öğrenmemek için iki kez ödersin.
- Quarantine, sessizce büyüyen bir devre dışı test yığını üretir. O yığın borçtur ve faiziyle büyür — her atlanan test, artık kimsenin koşmadığı bir kontrol.
- Ticket, backlog gürültüsüne yaşlanır ve sessizce önemini yitirir.
Hiçbiri testi deterministik yapmaz. Belirtiyi gizler ve bunun bedelini sana keser.
Patlamayı değil, anti-pattern’i yakala
Kararsız bir test nadiren ince bir sebepten patlar. Az sayıda, iyi bilinen anti-pattern yüzünden patlar — ve bunlar test daha çalışmadan, diff’te görünür. Sabit bir sleep, seed’siz bir RNG, duvar-saatine dayalı bir assertion, yapısal bir selector: her biri, test yazıldığı anda statik ve deterministik olarak tespit edilebilir.
Ben bu kontrolü commit sınırında çalıştırıyorum — değişen test dosyalarının eklenen satırları üzerinde statik bir tarama, bilinen anti-pattern’leri işaretliyor. Aynı diff, aynı sonuç; model yok, network yok. Akıllı bir race’i yakalamaz, zaten amacı bu değil. Amaç daha dar ama çok değerli: bariz kararsızlığı CI’a hiç ulaşmadan durdurmak — ki orada yakalamak yüz kat pahalı ve bir mühendisin öğleden sonrasına mal oluyor.
Testi, tespit edilecek bir şey kalmayacak şekilde tasarla
Tespit, emniyet ağı. Asıl çözüm, testi en baştan deterministik yazmak:
- Sabit beklemeleri koşula dayalı beklemeyle değiştir — beklediğin duruma ulaşana kadar yokla ya da çerçevenin
Eventually/waitFor’ını kullan. Saate değil, bir olguya bekle. - Her rastgele kaynağı seed’le; test ortasında
now()veMath.random()okumak yerine deterministik bir saat ve ID enjekte et. - DOM pozisyonuna değil, role veya test id’ye göre seç.
- Logic’i değil, sınırı mock’la — ve en az bir gerçek integration yolunu koru, yoksa süit var olmayan bir dünyayı doğrular.
Bu kalıp ne zaman doğru cevap olmaktan çıkar?
Statik bir kapı isabet-önceliklidir: bariz anti-pattern’leri yakalar, kendi kodunun içine gömülü bir race condition’ı değil. Kurallarını muhafazakâr tut — gürültülü bir kapı yok sayılır, yok sayılan bir kapı ise hiç olmamasından kötüdür. Akıllı kararsızlığı kaçıracaktır; tahmin etmesine izin verme.
Ve bunların hiçbiri daha zor olan okumanın yerine geçmez: test edilen sistem deterministik olmadığı için kararsız olan bir test, sana test hakkında bir şey söylemiyor. Sana sistem hakkında bir şey söylüyor.
Güvenmek için yeniden çalıştırman gereken yeşil bir CI, yeşil değildir. Kararsız bir testi öldürmenin en ucuz yeri, yazıldığı satır — sana bir build’e mal olmadan önce.
İlgili: burada anlatılan commit-sınırı kontrolünü CommitBrief’te shipledim — flaky-test dedektörü değişen test dosyalarındaki sabit sleep’leri ve seed’siz rastgeleliği, modelden önce, deterministik olarak statik yakalar.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.