70/20/10 neden yanlış soru
Süit yavaşladığında ilk refleks oran saymak oluyor: “E2E’miz %40, olması gereken %10.” Bu soru yanlış çünkü piramit bir dağılım reçetesi değil, bir maliyet-geri bildirim modeli. Söylediği tek şey şu: bir hatayı ne kadar geç ve ne kadar büyük bir bağlamda yakalarsan, onu bulmak ve düzeltmek o kadar pahalıdır. Oranlar bu modelin sonucudur, girdisi değil.
Oranı hedef alan ekip testin ne yakaladığına değil nerede sayıldığına bakmaya başlar. E2E sayısını düşürmek için testler silinir ama sildiğin riskin karşılığı alt seviyede yazılmaz; ya da tam tersi, yüzdeyi tutturmak için anlamsız unit testler üretilir. Sonuç hem yavaş hem boşluklu bir süittir: toplam süre düşmez, kaçak hata artar, ama tablo düzgün görünür. Doğru soru “oranımız kaç” değil, “bu test hangi riski, hangi maliyetle yakalıyor”.
Piramidin gerçek ekseni: risk sınıfı
Her test bir risk sınıfını yakalamak için yazılır. Pratikte beş sınıf işi görür:
- Hesaplama hatası. Kendi kodundaki mantık: fiyat, vergi, yetki kuralı, durum geçişi.
- Sözleşme uyumsuzluğu. Senin beklediğin alan adı/tip ile karşı tarafın gönderdiği arasındaki fark.
- Entegrasyon ve konfigürasyon hatası. Yanlış bağlantı dizesi, eksik migration, yanlış kuyruk adı, yanlış ortam değişkeni.
- Tarayıcı davranışı. Gerçek DOM, odak yönetimi, yönlendirme, çerez ve oturum.
- Veri durumu. Belirli bir kayıt kombinasyonunda ortaya çıkan davranış.
Seviye seçimi bu sınıfa göre yapılır. Kritik nokta şu: aynı riski iki seviyede yakalamak süreyi ikiye katlar, kapsamı artırmaz. İndirim kuralını unit testte doğruladıysan, aynı kuralı E2E’de bir kez daha doğrulamak sana yeni bilgi vermez; yalnızca aynı hata için iki kırmızı, iki bakım yükü ve iki kararsızlık kaynağı üretir. Test seviyeleri arasındaki fark kapsam genişliği değil, yakaladıkları risk türüdür.
Karar tablosu: bir testi hangi seviyeye yazacağına nasıl karar verilir
Oran yerine şu beş sütunu doldur. Seviye çıktı olarak gelir.
| Risk sınıfı | Hatanın kaynağı | Tespit için gereken en küçük bağlam | Kabul edilebilir geri bildirim süresi | Kararsızlık maliyeti | Seviye |
|---|---|---|---|---|---|
| Hesaplama hatası | Kendi kodun | Tek fonksiyon / tek sınıf | Saniyeler, her kayıtta | Çok yüksek (sık koşar) | Unit |
| Veri durumu | Kendi kodun + şema | Gerçek veritabanı, tek süreç | Dakika altı, her PR’da | Yüksek | Integration |
| Sözleşme uyumsuzluğu | Dış servis / başka takım | İstek-yanıt çifti, şema | Dakikalar, günlük + PR’da | Orta | Contract |
| Entegrasyon/konfig. hatası | Altyapı | Ayağa kalkmış servisler, ağ | Dakikalar, merge sonrası | Orta | Integration / smoke |
| Tarayıcı davranışı | Kendi kodun (arayüz) | Gerçek tarayıcı, tek sayfa | Dakikalar | Yüksek | Component / E2E (dar) |
| Çok sistemli iş akışı | Sistemler arası | Tüm yığın | 10+ dakika, merge sonrası | Çok yüksek | E2E |
Sütunları okuma sırası önemli. “Tespit için gereken en küçük bağlam” sütunu seviyeyi büyük ölçüde belirler; “kabul edilebilir geri bildirim süresi” testin ne zaman koşacağını; “kararsızlık maliyeti” ise testin hak ettiği yatırımı. Bir testi en küçük bağlamdan daha büyük bir seviyede yazıyorsan, fark ettiğin anda bunun gerekçesini yazabilmen gerekir.

Tabloyu üç örnek senaryoda çalıştırmak
1. İndirim hesaplaması. Risk sınıfı hesaplama hatası, kaynak kendi kodun, en küçük bağlam tek fonksiyon. Seviye: unit. Yazılmaması gereken test: “sepete üç ürün ekle, kupon gir, toplamı kontrol et” diye bir E2E.
// src/pricing/discount.ts
export type Sepet = {
araToplam: number;
urunAdedi: number;
musteriTipi: "standart" | "uye";
};
export function indirimOraniHesapla(sepet: Sepet): number {
let oran = 0;
if (sepet.musteriTipi === "uye") oran += 0.05;
if (sepet.araToplam >= 1000) oran += 0.10;
else if (sepet.araToplam >= 500) oran += 0.05;
if (sepet.urunAdedi >= 5) oran += 0.03;
return Math.min(oran, 0.20);
}
// src/pricing/discount.test.ts
import { describe, it, expect } from "vitest";
import { indirimOraniHesapla } from "./discount";
describe("indirimOraniHesapla", () => {
it("standart müşteri, eşik altı: indirim yok", () => {
expect(indirimOraniHesapla({ araToplam: 499, urunAdedi: 1, musteriTipi: "standart" })).toBe(0);
});
it("500 eşiğinde üye: 0.05 + 0.05", () => {
expect(indirimOraniHesapla({ araToplam: 500, urunAdedi: 1, musteriTipi: "uye" })).toBeCloseTo(0.10);
});
it("tavanı aşamaz", () => {
expect(indirimOraniHesapla({ araToplam: 5000, urunAdedi: 50, musteriTipi: "uye" })).toBe(0.20);
});
});
2. Ödeme sağlayıcısıyla entegrasyon. Risk sınıfı sözleşme uyumsuzluğu, kaynak dış servis, en küçük bağlam istek-yanıt çifti. Seviye: contract. Sağlayıcının sandbox’ına karşı şemayı doğrula; kendi hata eşleştirmen ayrıca unit testle kapanır. Yazılmaması gereken test: her ödeme hata kodu için ayrı bir E2E checkout senaryosu.
// test/contract/payment-provider.contract.test.ts
import { describe, it, expect } from "vitest";
import { z } from "zod";
const OdemeYanitSemasi = z.object({
id: z.string().min(1),
status: z.enum(["authorized", "declined", "pending"]),
amount: z.object({ value: z.number(), currency: z.string().length(3) }),
created_at: z.string().datetime(),
});
const BASE_URL = process.env.PAYMENT_SANDBOX_URL ?? "https://sandbox.example-payments.test";
const API_KEY = process.env.PAYMENT_SANDBOX_KEY ?? "test-key";
describe("ödeme sağlayıcısı sözleşmesi", () => {
it("authorize yanıtı beklenen şemaya uyar", async () => {
const res = await fetch(`${BASE_URL}/v1/payments`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${API_KEY}` },
body: JSON.stringify({ amount: { value: 1000, currency: "TRY" }, card_token: "tok_test_ok" }),
});
expect(res.status).toBe(201);
const parsed = OdemeYanitSemasi.safeParse(await res.json());
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
});
it("reddedilen kart da aynı şemayı döner", async () => {
const res = await fetch(`${BASE_URL}/v1/payments`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${API_KEY}` },
body: JSON.stringify({ amount: { value: 1000, currency: "TRY" }, card_token: "tok_test_declined" }),
});
const parsed = OdemeYanitSemasi.safeParse(await res.json());
expect(parsed.success).toBe(true);
if (parsed.success) expect(parsed.data.status).toBe("declined");
});
});
3. Çok adımlı kayıt akışı. Risk sınıfı çok sistemli iş akışı: form, kimlik servisi, e-posta gönderimi, oturum çerezi. Tek bir seviyede güvenilir taklidi yok. Seviye: E2E — ama tek bir mutlu yol. Alan doğrulama mesajları, parola kuralları ve hata metinleri component/unit seviyesine iner.
// e2e/kayit.spec.ts
import { test, expect, request } from "@playwright/test";
test("yeni kullanıcı kaydolur, e-postasını doğrular ve oturum açar", async ({ page, baseURL }) => {
const benzersiz = `kayit-${Date.now()}-${process.env.TEST_WORKER_INDEX ?? "0"}`;
const eposta = `${benzersiz}@ornek.test`;
const parola = "Gecerli-Parola-123";
await page.goto("/kayit");
await page.getByLabel("E-posta").fill(eposta);
await page.getByLabel("Parola", { exact: true }).fill(parola);
await page.getByRole("button", { name: "Hesap oluştur" }).click();
await expect(page.getByText("Doğrulama bağlantısı gönderildi")).toBeVisible();
const api = await request.newContext({ baseURL });
const res = await api.get(`/test-api/verification-token?email=${encodeURIComponent(eposta)}`);
expect(res.ok()).toBeTruthy();
const { token } = await res.json();
await api.dispose();
await page.goto(`/dogrula?token=${token}`);
await expect(page.getByRole("heading", { name: "Hesabın hazır" })).toBeVisible();
await page.goto("/giris");
await page.getByLabel("E-posta").fill(eposta);
await page.getByLabel("Parola", { exact: true }).fill(parola);
await page.getByRole("button", { name: "Giriş yap" }).click();
await expect(page.getByTestId("kullanici-menusu")).toContainText(eposta);
});
Bu testin kendi verisini üretmesi tesadüf değil; paralel koşumda tek paylaşılan hesap, karar tablosunun “kararsızlık maliyeti” sütununu doğrudan patlatır. Gerekçesi için test verisi yönetimi yazısına bak.
E2E’ye hak kazanan testin üç şartı
Bir test E2E’de ancak şu üçü birlikte sağlanıyorsa durur:
- Birden fazla sistemin birlikte çalışması riski taşır. Tek serviste doğrulanabilen davranış E2E’ye ait değildir.
- Hata gerçek kullanıcı için para veya veri kaybı anlamına gelir. Yanlış hizalanmış bir buton bu eşiği geçmez; ödenmiş siparişin kaydedilmemesi geçer.
- Daha alt bir seviyede güvenilir biçimde taklit edilemez. Taklit mümkünse alt seviye her zaman kazanır, çünkü daha hızlı ve daha kararlıdır.
Üçü birden sağlanmıyorsa test alt seviyeye iner. Bu kurala gelen en yaygın itiraz “ama E2E gerçek kullanıcıyı test ediyor” olur. Cevabı şudur: E2E gerçek kullanıcıyı değil, gerçek kurulumu test eder. Kullanıcıya dair bilgiyi veren şey testin seviyesi değil, senaryonun kendisidir; aynı senaryoyu alt seviyede koşturduğunda kullanıcı davranışına dair bilgiyi kaybetmezsin, yalnızca ayağa kaldırdığın altyapıyı kaybedersin. Kaybettiğin tek şey, ilgili riski gerçekten taşıyan entegrasyon noktalarıysa test E2E’de kalır — zaten üç şartın söylediği bu.
Mevcut süiti tabloyla yeniden sınıflandırmak
Sıfırdan piramit kurmaya çalışma; mevcut süitten başla.
- Etiketle. Her testi iki veriyle işaretle: medyan süresi ve son 30 günde yakaladığı gerçek hata sayısı. “Gerçek hata” = düzeltilmiş bir üretim/geliştirme hatasını gösteren kırmızı. Kendi kararsızlığından kaynaklanan kırmızılar sayılmaz, onlar ayrı bir sütun.
- Aday havuzu kur. Uzun süren ve 30 günde hiç gerçek hata yakalamamış E2E testleri havuza girer. Bu, “bu test gereksiz” demek değil; “bu testin gerekçesini yeniden yazmak zorundasın” demektir.
- Risk sınıfını belirle. Her aday için karar tablosunun beş sütununu doldur.
- Taşı veya sil. Alt seviyede karşılığı yazılabiliyorsa taşı. Taşımadan silme; silme kriteri açık olmalı.
Silme kriteri: test, başka bir testin yakaladığı riski tekrar yakalıyorsa (çift kapsama) veya artık var olmayan bir davranışı doğruluyorsa silinir. Sırf yavaş olduğu veya sırf kararsız olduğu için silinmez — yavaşlık taşıma sebebidir, kararsızlık ise teşhis edilmesi gereken bir semptomdur.
Taşıma sonrası kaçak hata görülüyorsa bunun tipik kaynağı tek bir sütundur: “tespit için gereken en küçük bağlam” sütununun fazla iyimser doldurulması. Ekipler sahteleyerek (stub) kapattıkları bir entegrasyonun aslında gerçek bir konfigürasyon riski taşıdığını sonradan fark eder. Bu durumda doğru hamle E2E’yi geri getirmek değil, o riski kapsayan dar bir smoke testi eklemektir.
Taşıma kararını ölçen metrikler
Oran takip etmeyi bırak, şu dördünü ölç:
- PR başına geri bildirim süresi. Geliştiricinin push’tan sonra sonucu görme süresi. Piramit doğruysa bu süre düşer. Tipik örüntü şudur: E2E’den alt seviyeye taşınan her test, süreden kendi koşum süresinin birkaç katını siler; çünkü yalnızca test değil, ortam kurulumu, veri hazırlığı ve yeniden deneme payı da kalkar.
- Kaçak hata sayısı ve seviyesi. Yalnızca sayıyı değil, “bu hatayı hangi seviye yakalamalıydı” cevabını da kaydet. Bu alan karar tablonun hangi sütununu yanlış doldurduğunu söyler.
- Test başına kararsızlık oranı. Toplam flaky oranı yanıltır; suçluyu test bazında gör.
- Çift kapsama. Aynı hatayı aynı anda kaç test yakalıyor? İkiden fazlaysa orada silinecek test var.
Süreyi düşürmenin ikinci kolu da paralelleştirme ve koşum maliyetidir; test otomasyonunda performans optimizasyonu tarafı bu metriklerle birlikte okunmalı.
Piramidin bozulduğu yerler: kum saati ve dondurma külahı
İki bozulma da tek bir sebepten çıkar: karar tablosunun bir sütununun hiç doldurulmaması.
Kum saati, orta katmanın boş olmasıdır. Çok unit, çok E2E, neredeyse sıfır entegrasyon ve sözleşme testi. Sebep: “tespit için gereken en küçük bağlam” sütununun atlanması. Ekip iki uç seçenek tanır — tek fonksiyon ya da tüm sistem — ve arada kalan her riski E2E’ye yıkar. Çözüm API ve sözleşme testleriyle orta katmanı doldurmaktır.
Dondurma külahı, E2E’nin unit boşluğunu kapatmaya çalışmasıdır. Sebep: “hatanın kaynağı” sütununun atlanması. Kendi kodundaki mantık hatası, kaynağı belli olmasına rağmen arayüzden doğrulanır; her kural değişikliğinde tarayıcı testi yazılır.

Bu haftaki ilk adım
CI raporundan en yavaş 10 E2E testini çıkar. Her biri için karar tablosunun beş sütununu doldur: risk sınıfı, hatanın kaynağı, tespit için gereken en küçük bağlam, kabul edilebilir geri bildirim süresi, kararsızlık maliyeti. Üç şartı birlikte sağlamayan her testi işaretle.
Sonra en az üçünü alt seviyeye taşı ya da sil — taşıdığın riskin karşılığını aynı PR içinde yaz, boş bırakma. Taşımadan önce PR başına geri bildirim süresinin medyanını kaydet, iki hafta sonra aynı ölçümü tekrarla ve yanına kaçak hata sayısını koy. Süre düşmüş, kaçak hata artmamışsa karar tablon doğru çalışıyor demektir; sıradaki 10 teste geç.