Regresyon seti büyüdükçe ekip iki şikâyeti aynı anda duymaya başlar: “PR’ım bir saat bekliyor” ve “CI faturası neden bu kadar arttı”. İlk refleks listeyi açıp en gereksiz görünen testleri silmektir. Bu refleks, silinen testin yakaladığı hata üretime kaçana kadar doğru görünür; sonra bütün girişim geri alınır ve ekip bir daha bu konuya dokunmaz.
Regresyon setini küçültmek bir mühendislik kararıdır, temizlik işi değil. Kararın meşru olması için silmeden önce ölçülmüş altı şey gerekir: hata yakalama verimi, kararsızlık oranı, süre dağılımı, kapsam çakışması, kaçan hata baz çizgisi ve parasal maliyet. Bunlar elinizde yoksa yaptığınız şey optimizasyon değil, kumardır.
Önce kararı tanımla: neyi optimize ediyorsun?
“Regresyon setini küçültelim” cümlesi üç farklı problemden doğar ve üçü farklı testleri hedefler.
PR geri bildirim süresi. Geliştirici değişikliği gönderdikten sonra sonucu kaç dakikada görüyor? Burada toplam makine süresi değil, kritik yoldaki en uzun dal önemlidir. Hedef testler: PR setindeki en yavaş E2E senaryoları.
CI faturası. Toplam makine dakikası. Burada paralel dalların kısalığı işe yaramaz; önemli olan tüketilen dakika toplamıdır. Hedef testler: çok sık koşan, çok sayıda, tek tek kısa ama toplamda pahalı testler.
Bakım yükü. Ekibin haftada kaç saatini test düzeltmeye harcadığı. Hedef testler: kırılgan selector’lara, paylaşılan test verisine ve zamanlamaya bağlı olanlar.

Hedefi sayıyla yaz: “PR pipeline’ı 42 dakikadan 12 dakikaya.” Sayı yoksa küçültme keyfi silmeye dönüşür; ilk kaçan hatada kimse neyi niçin sildiğini savunamaz.
Test envanteri: her test için tutulması gereken altı alan
Karar vermek için test başına şu altı alan gerekir:
- Ortalama ve p95 süre. Ortalama tek başına yanıltır; p95 pipeline’ı bekleten kuyruğu gösterir.
- Son 30-90 koşudaki başarısızlık sayısı.
- Bu başarısızlıkların kaçı gerçek hata. Geri kalanı kararsızlık ya da ortam sorunudur.
- Son gerçek hata tarihi.
- Kapsadığı iş kuralı veya risk. Serbest metin değil, sabit bir etiket listesi.
- Piramitteki seviyesi. Unit, entegrasyon, API, E2E.
Dördüncü ve beşinci alanlar dışındakiler CI’ın zaten ürettiği rapor dosyalarından çıkar; yeni bir altyapı gerekmez. Pratikte engel ölçüm değil saklamadır: artifact retention süresi çoğu kurulumda birkaç haftadır ve shard başına ayrı rapor dosyası üretildiği için tek koşunun verisi parçalıdır. İkisi de çözülebilir — retention’ı uzat, shard raporlarını koşu kimliğiyle aynı klasöre topla. Dört ve beşinci alanlar insan işidir; onları test adına ya da etikete gömmek en ucuz yoldur.
Bu tablo yoksa toplantıda yapılan şey ölçüm değil tahmindir ve tahminde en yüksek sesli kişi kazanır.
Hata yakalama verimi: test başına gerçek bulgu
Bir testin değeri çalıştığı için değil, yakaladığı gerçek regresyon sayısıyla ölçülür. Son 6-12 ayda hiç gerçek hata yakalamamış bir test, sıfır kanıtla maliyet üretiyor demektir.
Ama “hiç patlamadı = sil” kestirmesi yanlıştır. Ödeme akışı, yetkilendirme kontrolleri ve veri bütünlüğü gibi yollar düşük frekanslı ama yüksek etkilidir: orada bir regresyonun yılda bir kez olması, olduğunda ucuz olduğu anlamına gelmez. Bu testlerin değeri sigorta değeridir ve yakalama sayısından bağımsızdır.
Dolayısıyla verimi tek başına değil, risk ağırlığıyla çarparak oku. Pratik bir ayrım: testin koruduğu başarısızlık para kaybı, veri kaybı veya yasal yükümlülük üretiyorsa yield sıfır olsa bile silme listesine giremez. Üretmiyorsa sıfır yield gerçek bir silme gerekçesidir.
Flaky oranı: küçültmeden önce temizlenmesi gereken gürültü
Kararsız testler iki şeyi birden bozar: retry’larla süreyi uzatır, başarısızlık sinyaline olan güveni düşürür. Daha kötüsü, envanterdeki bütün sayıları kirletirler. Haftada üç kez rastgele patlayan bir test, “başarısızlık sayısı” sütununda en çok hata yakalayan test gibi görünür. O sütunu düzeltmeden alınan hiçbir karar güvenilir değildir.
Sıra şu: önce test başına flake oranını hesapla (aynı commit üzerinde durum değiştirme sıklığı), bir eşik belirle — örneğin %2 — ve eşiği aşanları ya düzelt ya karantinaya al. Kök nedeni bulmadan retry eklemek sorunu sayıdan siler, setten silmez; nedeni nasıl bulacağını flaky testleri teşhis etmek yazısında ayrıca ele aldım. Kararsızlığın en yaygın kaynağı paylaşılan test verisidir; her testin kendi verisini üretmesi bu nedenle pazarlık konusu değildir.
Kirli veriyle alınan silme kararı, yanlış testi siler.
Süre dağılımı ve kritik yol
Toplam sürenin ortalaması yanıltıcıdır. Geliştiricinin beklediği süreyi en uzun paralel dal belirler; diğer shard’lar üç dakikada bitse bile pipeline en yavaş dalın süresidir.

Testleri süreye göre sırala. Tipik bir sette sürenin büyük kısmını az sayıda test üretir ve bu testlerin önemli bir bölümü silinmeden hızlanabilir:
- Kurulumu API’ye taşı. UI üzerinden kullanıcı oluşturan, kategori açan, sepet dolduran adımlar HTTP çağrısına çevrilir.
- UI login’i token’a çevir. Oturumu bir kez al, depolama durumunu tüm testlere ver.
- Shard dağılımını süreye göre dengele. Dosya sayısına göre bölmek, 2 dakikalık dalın yanında 20 dakikalık dal üretir.
Silmeden önce bu listeyi tüket; test otomasyonunda performans optimizasyonu yazısındaki tekniklerin çoğu kapsamdan hiçbir şey kaybettirmeden süreyi aşağı çeker. Kapsam kaybı olmadan alınan kazanç bedavadır; sildiğin test ise geri gelmez.
Kapsam çakışması: aynı riski kaç test doğruluyor
En güvenli kesinti satır kapsamından değil, çakışmadan gelir. Bir iş kuralı — diyelim “indirim kuponu yalnızca bir kez kullanılabilir” — hem unit testinde, hem API testinde, hem de E2E senaryosunda doğrulanıyorsa, E2E katmanı çoğu zaman gereksizdir. E2E’nin asıl işi kuralın doğruluğunu değil, katmanların birbirine bağlandığını göstermektir; kuralın kendisi zaten alt seviyede ve daha hızlı doğrulanıyor.
Çakışmayı kod kapsamı aracıyla bulamazsın. Kapsam aracı aynı satırlardan geçen iki testi aynı sayar, ama o testlerin niyetini ayıramaz: biri kuralı doğruluyor, diğeri entegrasyonu. Bu yüzden eşleştirmeyi iş kuralı etiketi üzerinden yap — envanterdeki beşinci alan tam olarak bunun için var. Aynı etikete sahip testleri yan yana koy, en alt seviyedekini koru, üsttekileri değerlendirmeye al.
Kaçan hata oranı: küçültmenin tek dürüst karnesi
Küçültmenin başarısının tek dürüst ölçüsü, üretime kaçan hata sayısıdır. Ama her kaçan hata sayılmaz: yalnızca regresyon seti tarafından yakalanabilir olanlar. Yeni özellikteki tasarım hatası bu sayıya girmez.
Baz çizgiyi kesintiden önce ölç ve en az bir çeyreklik geçmişten al. Her kaçan hataya tek bir etiket ekle: “hangi test seviyesi bunu yakalamalıydı?” — unit, API, E2E veya “hiçbiri, test edilebilir değildi”. Bu etiket iki işe yarar: baz çizgiyi karşılaştırılabilir kılar ve kesintiden sonra hangi katmanı geri açacağını söyler.
Baz çizgisi olmayan bir kesinti geri dönüşsüzdür, çünkü sonradan “durum kötüleşti mi” sorusunu cevaplayacak referansın olmaz. Tartışma sezgiye döner ve sezgi her zaman en son yaşanan olaya göre konuşur.
CI maliyetini paraya çevirmek
Formül basit: dakika ücreti × koşu süresi × günlük koşu sayısı × ay içindeki iş günü. Bu sayı, yöneticiyle konuşurken sahip olduğun tek ortak dildir.
Yanına ikinci bir kalem koy: geliştiricinin bekleme maliyeti. Gün içinde kaç kez pipeline beklenerek bağlam kaybediliyor? Bekleme süresi 10 dakikayı aştığında geliştirici başka işe geçer ve geri dönüş maliyeti beklenen süreden uzundur. Bunu “kişi × günlük bekleme sayısı × saat ücreti” olarak yaz; kaba bir tahmin bile sıfırdan iyidir.
Üçüncü kalem şart: testi silmenin riski de parasal yazılmalı. Beklenen maliyet = regresyonun üretimde oluşma olasılığı × oluştuğunda doğan maliyet (hotfix, destek, iade, itibar). Bu satırı tabloya koymazsan maliyet tarafı tek başına sayısal kalır, kalite tarafı sezgisel kalır ve sayısal olan her zaman kazanır.
Ölçümü toplayan küçük bir betik
Aşağıdaki betik, CI’ın ürettiği JUnit XML ve Playwright JSON raporlarını bir klasörde biriktirdiğin varsayımıyla çalışır ve test başına süre, başarısızlık ve kararsızlık özetini CSV olarak verir. Amacı dashboard değil; karar toplantısına gerçek veriyle girmek.
#!/usr/bin/env python3
"""CI rapor arsivinden test envanteri cikarir.
Kullanim: python envanter.py rapor-arsivi/ > envanter.csv
"""
import csv
import json
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from pathlib import Path
def yeni_kayit():
return {"sureler": [], "kosu": 0, "basarisiz": 0, "flaky": 0}
def p95(degerler):
if not degerler:
return 0.0
sirali = sorted(degerler)
indeks = int(round(0.95 * (len(sirali) - 1)))
return sirali[max(0, min(indeks, len(sirali) - 1))]
def junit_oku(dosya, kayitlar):
kok = ET.parse(dosya).getroot()
for vaka in kok.iter("testcase"):
ad = "{}::{}".format(vaka.get("classname") or "", vaka.get("name") or "")
kayit = kayitlar[ad]
kayit["kosu"] += 1
kayit["sureler"].append(float(vaka.get("time") or 0.0))
if vaka.find("failure") is not None or vaka.find("error") is not None:
kayit["basarisiz"] += 1
if vaka.find("flakyFailure") is not None or vaka.find("rerunFailure") is not None:
kayit["flaky"] += 1
def suite_gez(suite, kayitlar, onek=""):
baslik = "{}{} > ".format(onek, suite.get("title") or "")
for spec in suite.get("specs", []):
ad = baslik + (spec.get("title") or "")
for test in spec.get("tests", []):
kayit = kayitlar[ad]
kayit["kosu"] += 1
sonuclar = test.get("results", [])
kayit["sureler"].append(sum(s.get("duration", 0) for s in sonuclar) / 1000.0)
if test.get("status") == "unexpected":
kayit["basarisiz"] += 1
if test.get("status") == "flaky":
kayit["flaky"] += 1
for alt in suite.get("suites", []):
suite_gez(alt, kayitlar, baslik)
def playwright_oku(dosya, kayitlar):
veri = json.loads(Path(dosya).read_text(encoding="utf-8"))
for suite in veri.get("suites", []):
suite_gez(suite, kayitlar)
def main(klasor):
kayitlar = defaultdict(yeni_kayit)
for dosya in sorted(Path(klasor).rglob("*")):
try:
if dosya.suffix == ".xml":
junit_oku(dosya, kayitlar)
elif dosya.suffix == ".json":
playwright_oku(dosya, kayitlar)
except (ET.ParseError, json.JSONDecodeError, KeyError):
print("atlandi: {}".format(dosya), file=sys.stderr)
yazici = csv.writer(sys.stdout)
yazici.writerow(
["test", "kosu", "ortalama_sn", "p95_sn", "basarisiz", "flaky", "flake_orani"]
)
siralanmis = sorted(
kayitlar.items(), key=lambda x: sum(x[1]["sureler"]), reverse=True
)
for ad, k in siralanmis:
ortalama = sum(k["sureler"]) / len(k["sureler"]) if k["sureler"] else 0.0
oran = k["flaky"] / k["kosu"] if k["kosu"] else 0.0
yazici.writerow(
[
ad,
k["kosu"],
round(ortalama, 2),
round(p95(k["sureler"]), 2),
k["basarisiz"],
k["flaky"],
round(oran, 4),
]
)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Kullanim: python envanter.py rapor-arsivi/", file=sys.stderr)
sys.exit(1)
main(sys.argv[1])
Bu betik hafta içinde kurulur, iki haftalık veri biriktirir. Gerçek hata/kararsızlık ayrımı gibi insan kararı gerektiren alanları CSV’ye elle iki sütun olarak ekle; başlangıç için yeterli.
Karar matrisi: sil, aşağı taşı, seyrekleştir, koru
Her test için dört sonuçtan biri verilir ve gerekçesi envanterden gelir.

Silme yalnızca üç koşul birlikte sağlandığında meşrudur: kapsam çakışması + sıfır yield + düşük risk. İkisi varsa yetmez. Yüksek değerli ama yavaş testler silinmez, alt seviyeye taşınır — aynı kural daha ucuz doğrulanır. Düşük frekanslı yüksek etkili riskler nightly’ye alınır. Kritik iş akışları ne olursa olsun PR setinde kalır; orada süre tartışması yapılmaz, hızlandırma yapılır.
Bu matrisi uygularken en çok yanıltan metrik “son 12 ayda hiç patlamadı” olur. Bu, testin değersiz olduğunu değil, koruduğu alanın o dönemde değişmediğini gösterebilir. Modül bir sonraki çeyrekte refactor edilecekse test tam da o anda değer üretecektir. Yield’i her zaman yol haritasıyla birlikte oku.
Seyrekleştirmeyi etiketle uygulamak
Silmek yerine etiketlemek, kararı tek satırla geri alınabilir kılar.
// tests/odeme.spec.ts
import { test, expect } from '@playwright/test';
test('kredi kartı ile sipariş tamamlanır', { tag: ['@kritik', '@pr'] }, async ({ page }) => {
await page.goto('/sepet');
await page.getByRole('button', { name: 'Ödemeye geç' }).click();
await expect(page.getByText('Siparişiniz alındı')).toBeVisible();
});
test('kupon ikinci kez kullanılamaz', { tag: ['@nightly'] }, async ({ page }) => {
await page.goto('/sepet');
await page.getByLabel('Kupon kodu').fill('TEKRAR10');
await page.getByRole('button', { name: 'Uygula' }).click();
await expect(page.getByText('Bu kupon daha önce kullanıldı')).toBeVisible();
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
const KADEMELER: Record<string, RegExp> = {
pr: /@pr/,
nightly: /@pr|@nightly/,
release: /.*/,
};
const kademe = process.env.TEST_KADEMESI ?? 'pr';
export default defineConfig({
testDir: './tests',
grep: KADEMELER[kademe] ?? KADEMELER.pr,
retries: kademe === 'pr' ? 0 : 1,
workers: process.env.CI ? 4 : undefined,
reporter: [
['junit', { outputFile: `raporlar/${kademe}-junit.xml` }],
['json', { outputFile: `raporlar/${kademe}.json` }],
['list'],
],
});
# .github/workflows/testler.yml
name: testler
on:
pull_request:
schedule:
- cron: '0 2 * * *'
jobs:
kos:
runs-on: ubuntu-latest
env:
TEST_KADEMESI: ${{ github.event_name == 'schedule' && 'nightly' || 'pr' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: raporlar-${{ env.TEST_KADEMESI }}-${{ github.run_id }}
path: raporlar/
retention-days: 90
retention-days: 90 tesadüf değil: envanterin veri kaynağı bu arşiv. Set büyüdükçe etiket şemasını klasör yapısıyla hizalamak gerekir; test otomasyonunda ölçeklenebilirlik yazısındaki yapı burada doğrudan işe yarar.
Küçültmeden sonraki doğrulama penceresi
Kesinti canlıya çıktıktan sonra en az dört hafta boyunca üç sayıyı izle: kaçan hata oranı, hotfix sayısı ve pipeline süresi. Üçüncüsü iyileşirken ilk ikisi bozuluyorsa kazanç gerçek değil, borçtur.
Kaçan hata baz çizgiyi aşarsa yapılacak iş bellidir: hatayı envanterdeki iş kuralı etiketiyle eşleştir, o etiketi taşıyan silinmiş testleri bul ve nightly’ye geri al. Tamamını PR setine döndürme — tek bir olay bütün kararı geçersiz kılmaz, yalnızca o dalı geçersiz kılar.
Geri alma planını kesintiyle aynı gün yaz. Silinen testler bir branch’te değil, geçmişi okunabilir tek bir commit’te dursun; “hangi testler silindi” sorusunun cevabı dört hafta sonra git log’da aranmamalı.
Bu hafta ne yapacaksın
- CI’ın rapor arşivini aç, retention süresini en az 90 güne çıkar ve iki haftalık veri biriktirmeye başla.
- Betiği kur, ilk CSV’yi üret, süreye göre sıralı ilk 20 testi toplantıya getir.
- Flake eşiğini belirle (başlangıç için %2) ve eşiği aşanları karantinaya al. Bu adım bitmeden silme tartışması açma.
- Son bir çeyreğin kaçan hatalarını çıkar, her birine “hangi seviye yakalamalıydı” etiketi ekle. Bu senin baz çizgin.
- Karar matrisini tek bir modül üzerinde pilot uygula. Silme değil, önce etiketleme yap.
- Dört hafta izle, sonra ikinci modüle geç.
Tüm sete aynı anda yayma. Tek takımda işleyen bir yöntemin kanıtı, on takımda tartışmayı bitirir; kanıtsız bir politika ise ilk itirazda çöker.