---
title: "Kararsız Testler: Retry Çözüm Değil"
description: "AI test yazmayı neredeyse bedava yaptı, kararsız süitler patladı. Retry ve quarantine neden bir koşu bandı ve kararsızlığı deterministik olarak ne yakalar"
url: https://sade.dev/tr/journal/kararsiz-testler-ve-determinizm/
lang: tr
author: "Muhammet Şafak"
published: 2026-06-17
updated: 2026-09-06
section: Journal
tags: ["testing","ci","ai","quality"]
summary: "Retry, quarantine ve bir ticket kararsız bir testi deterministik yapmaz; CI zaten kırıldıktan sonra belirtiyi gizler. Kararsızlığın çoğu az sayıda bilinen anti-pattern'den gelir — sabit bir sleep, seed'siz rastgelelik, duvar saatine dayalı bir assertion, yapısal bir selector — ve her biri test yazıldığı anda diff'te statik olarak görülebilir. En ucuz yakalama yeri o satırdır."
---

# Kararsız Testler: Retry Çözüm Değil

> Retry, quarantine ve bir ticket kararsız bir testi deterministik yapmaz; CI zaten kırıldıktan sonra belirtiyi gizler. Kararsızlığın çoğu az sayıda bilinen anti-pattern'den gelir — sabit bir sleep, seed'siz rastgelelik, duvar saatine dayalı bir assertion, yapısal bir selector — ve her biri test yazıldığı anda diff'te statik olarak görülebilir. En ucuz yakalama yeri o satırdır.

İ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 bir `faker`.
- 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 birbirinden 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. Saati değil, bir olguyu bekle.
- Her rastgele kaynağı **seed**'le; test ortasında `now()` ve `Math.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](https://commitbrief.com)'te shipledim — [flaky-test dedektörü](https://github.com/CommitBrief/commitbrief) değişen test dosyalarındaki sabit sleep'leri ve seed'siz rastgeleliği, modelden önce, deterministik olarak statik yakalar.

## Sık sorulanlar

**Bir test süiti büyüdükçe neden daha az güvenilir oluyor?**

Çünkü ihtimaller birikiyor. %0,5 ihtimalle boşuna patlayan tek bir test görünmez; aynısından 800 tane, temiz bir çalıştırmada herhangi bir testin patlama ihtimalini %98'e yaklaştırır. Patlamalar birbirinden bağımsız da değil — CI paylaşımlı ve throttle'lı donanımda koşar; yerelde 50 ms'de biten async test yük altında timeout'a düşer.

**Kararsız bir testi quarantine etmenin nesi yanlış?**

Quarantine bir tedavi değil, triyaj. Yığın sessizce büyür ve atlanan her test artık kimsenin koşmadığı bir kontroldür; yani hata yakalama sisteminiz aşınırken ticket da backlog gürültüsüne yaşlanır. Bu sırada test hâlâ deterministik değildir; quarantine bunu değiştirmemiştir.

**Statik bir kontrol neyi yakalayamaz?**

Kendi kodunuza gömülü bir race condition'ı ve bilinen bir anti-pattern'e benzemeyen akıllı kararsızlıkları. Kuralları muhafazakâr tutun — gürültülü bir kapı yok sayılır, yok sayılan bir kapı ise hiç olmamasından kötüdür. Ve test edilen sistem deterministik olmadığı için kararsız olan bir test, size test hakkında değil sistem hakkında bir şey söylüyordur.


## Kaynaklar

- [Eradicating Non-Determinism in Tests: quarantine the test, but fix quarantined tests quickly](https://martinfowler.com/articles/nonDeterminism.html) — Martin Fowler
- [Playwright locators: prefer role and test id over selectors tied to DOM structure, and rely on auto-waiting instead of fixed timeouts](https://playwright.dev/docs/locators) — Playwright
