İçeriğe geç

Kabul kriterlerini teste çevirmek: Given-When-Then’den çalışan senaryoya

Muğlak kabul kriteri neye mal oluyor

“Kullanıcı geçersiz bilgiyle giriş yapamamalı” cümlesi bir kabul kriteri değil, bir temennidir. Geçersiz olan ne: boş alan mı, yanlış parola mı, kilitli hesap mı, silinmiş kullanıcı mı? “Yapamamalı” ne demek: hata mesajı mı görür, aynı sayfada mı kalır, üçüncü denemede hesap mı kilitlenir? Bu cümleyi iki testçi iki farklı şekilde test eder, üçüncüsü “geliştirici zaten bakmıştır” deyip hiç test etmez.

Muğlaklığın asıl bedeli üretimde çıkan hata değil. Asıl bedel, tartışılan bug kayıtları ve yeniden yazılan koddur. Tipik görünümü şudur: testçi bir davranışı hata olarak açar, geliştirici “kriterde böyle yazmıyor” der, ürün sahibi üç gün sonra “ben bunu kastetmiştim” diye araya girer, hikâye sprint sonunda yeniden açılır. Hiç kimse yanlış iş yapmamıştır; sadece aynı cümleyi üç farklı okumuşlardır. Kabul kriterinin tek görevi, bitti/bitmedi kararını tek bir cümleyle verilebilir hale getirmektir. Bunu yapmayan kriter, yazılmamış kriterdir.

Test edilebilir kabul kriterinin dört şartı

Bir kriterin teste çevrilebilmesi için dört şey gerekir.

Gözlemlenebilir çıktı. Sistemin dışarıdan görülen davranışı olmalı: ekranda bir mesaj, bir HTTP durum kodu, bir e-posta, bir dosya. “Sipariş doğru işlenmeli” gözlemlenebilir değil; “sipariş durumu HAZIRLANIYOR olarak görünür” gözlemlenebilir.

Belirli başlangıç durumu. Hangi veriyle başlıyoruz, tek cümleyle yazılabilmeli. “Kullanıcı sisteme kayıtlıdır” yetmez; “aktif, e-postası doğrulanmış ve sepetinde 500 TL tutarında ürün bulunan bir kullanıcı vardır” yeter.

Tek tetikleyici eylem. Senaryoyu başlatan bir eylem olmalı, iki değil. “Kullanıcı kuponu uygular ve ödemeye geçer” iki senaryodur.

İkili sonuç. Sonuç doğru ya da yanlış olarak cevaplanabilmeli. “Hızlı yüklenmeli” ikili değil; “arama sonuçları 2 saniye içinde döner” ikili.

“Kullanıcı dostu olmalı” dördünü de karşılamaz, “doğru çalışmalı” hiçbirini. Bu tür cümleler kriter değil, hikâyenin amacıdır; açıklama alanında kalsınlar.

Given-When-Then’i doğru kullanmak

Given-When-Then bir yazım şablonu değil, bir düşünme disiplinidir. Şablonu doldurup muğlaklığı korumak tamamen mümkündür. Üç parçanın görevi nettir: Given sistemin durumudur, eylem değil. When tek bir tetikleyicidir. Then gözlemlenebilir sonuçtur.

En yaygın üç hata:

Given içine kullanıcı adımı koymak. “Given kullanıcı giriş sayfasını açar ve bilgilerini girer” yazıldığında, hazırlık adımları testin gövdesine taşınır ve her senaryo aynı UI akışını baştan koşar. Giriş ekranındaki bir değişiklik, girişle hiç ilgisi olmayan otuz senaryoyu birden kırar. Durum olarak yazıldığında (“Given oturumu açık bir kullanıcı vardır”) aynı hazırlık API ile yapılabilir ve kırılganlık ortadan kalkar.

Birden fazla When zincirlemek. “When kullanıcı kuponu uygular, sonra adres seçer, sonra ödeme yapar” senaryosu düştüğünde hangi iş kuralının bozulduğu raporda okunmaz. Hata ayıklama, testi tekrar koşup ekran görüntüsüne bakmaya döner. Her When’e bir senaryo.

Then’de iç veritabanı durumu doğrulamak. “Then orders tablosunda status=2 olur” yazmak, testi uygulamanın iç yapısına bağlar. Kolon adı değiştiğinde davranış bozulmadığı halde test düşer; dahası, kullanıcı o satırı hiç görmez. Doğrulama, sistemin sözleşmesi üzerinden yapılmalı: API cevabı veya ekran.

Muğlak kriteri senaryoya çevirmek: önce ve sonra

Hikâye: Müşteri olarak sepetime indirim kuponu uygulamak istiyorum.

Muğlak haldeki kriterler genelde şöyle gelir:

  1. Kullanıcı geçerli bir kupon girebilmeli.
  2. Geçersiz kuponlar kabul edilmemeli.
  3. İndirim doğru hesaplanmalı.

Dönüşümü üç soruyla yapıyoruz: Hangi başlangıç verisi? Hangi tek eylem? Hangi tam mesaj, durum kodu veya ekran?

Birincisi için: hangi kupon, hangi sepet? Soruyu sorduğunuz anda ürün sahibi kuralı söylemek zorunda kalır.

Senaryo: Yüzdelik kupon sepet toplamına uygulanır
Given sepetinde 500 TL tutarında ürün bulunan bir kullanıcı vardır
And BAHAR10 kuponu aktiftir ve %10 indirim tanımlıdır
When kullanıcı sepet sayfasında BAHAR10 kuponunu uygular
Then sepet toplamı 450,00 TL olarak görünür
And “Kupon uygulandı: %10 indirim” mesajı görünür

İkincisi tek senaryo değil, bir aileydi. “Geçersiz” kelimesini açtığınızda üç ayrı kural çıkar: tanımsız kod, süresi dolmuş kupon, minimum tutarı karşılamayan sepet. Bu sınır durumları refinement sırasında ortaya çıkar; çünkü “geçersiz kuponlar kabul edilmemeli” cümlesini senaryoya çevirmeye kalkan kişi, hangi mesajın görüneceğini sormak zorunda kalır ve ürün sahibi süresi dolmuş kupon için ayrı bir metin istediğini orada fark eder.

Senaryo: Süresi dolmuş kupon reddedilir
Given sepetinde 500 TL tutarında ürün bulunan bir kullanıcı vardır
And KIS20 kuponunun geçerlilik tarihi dünde sona ermiştir
When kullanıcı KIS20 kuponunu uygular
Then “Bu kuponun süresi dolmuş” mesajı görünür
And sepet toplamı 500,00 TL olarak kalır

Üçüncüsü (“indirim doğru hesaplanmalı”) tek senaryoya sığmaz, örnek tablosuna dönüşür. Ona birazdan geleceğiz.

Her senaryo E2E testi değildir: kriteri doğru seviyeye yerleştirmek

Kabul kriterlerinin tamamını UI üzerinden otomatikleştirmek test setini yavaşlatır ve kırılganlaştırır. Kupon örneğinde yuvarlama kuralını tarayıcı açarak doğrulamanın hiçbir ek bilgi değeri yoktur; aynı kuralı milisaniyeler içinde birim testiyle kapatabilirsiniz.

Karar kuralı basit: iş kuralı varyasyonları birim veya servis seviyesinde, veri akışı ve entegrasyon API seviyesinde, kullanıcının gerçekten gördüğü kritik yolculuk E2E’de. Seviyelerin tanımı ve aralarındaki iş bölümü için test seviyeleri yazısına bakabilirsiniz.

Aynı hikâyedeki beş kriterin dağılımı şöyle olur:

  • Yüzde hesabı ve kuruş yuvarlaması → birim testi
  • Minimum sepet tutarı kuralı → birim testi
  • Süresi dolmuş kuponun reddi → API testi (POST /sepetler/{id}/kupon → 422 ve hata kodu)
  • Kuponun siparişe taşınması ve ödeme tutarının güncellenmesi → API testi
  • Kullanıcının sepet sayfasında indirimli toplamı ve mesajı görmesi → E2E testi

API seviyesinin nasıl kurulacağı için API test otomasyonu yazısı işinizi görür.

Kabul kriteri: iş kuralı varyasyonu→Birim / servis testi, servisler arası veri akışı→API testi, kritik yolculuk→E2E testi

Senaryodan çalışan teste: Playwright ile iskelet

Hedef şu: test kodu okunduğunda kabul kriterinin cümlesi geri okunabilsin. Given bir hazırlık fonksiyonu, When tek bir eylem, Then bir expect zinciri olur. Veri hazırlığı API ile yapılır; UI yalnızca doğrulanan şey için kullanılır.

// tests/kupon-uygula.spec.ts
import { test, expect, type APIRequestContext, type Page } from '@playwright/test';

const API = process.env.API_URL ?? 'http://localhost:3000/api';

type Baglam = { sepetId: string; token: string };

// --- Given: sistemin başlangıç durumu ---
async function sepetiOlanKullanici(
  request: APIRequestContext,
  sepetToplami: number
): Promise<Baglam> {
  const kayit = await request.post(`${API}/test/kullanicilar`, {
    data: { eposta: `kupon-${Date.now()}-${Math.random().toString(16).slice(2)}@ornek.test`,
            sifre: 'Test1234!' },
  });
  expect(kayit.ok(), 'test kullanıcısı oluşturulamadı').toBeTruthy();
  const { token } = await kayit.json();

  const sepet = await request.post(`${API}/sepetler`, {
    headers: { Authorization: `Bearer ${token}` },
    data: { urunler: [{ sku: 'TEST-SKU-1', adet: 1, birimFiyat: sepetToplami }] },
  });
  expect(sepet.ok(), 'sepet oluşturulamadı').toBeTruthy();
  const { id: sepetId } = await sepet.json();

  return { sepetId, token };
}

async function kuponTanimla(
  request: APIRequestContext,
  kupon: { kod: string; yuzde: number; bitisTarihi: string; minimumTutar?: number }
): Promise<void> {
  const cevap = await request.post(`${API}/test/kuponlar`, { data: kupon });
  expect(cevap.ok(), `kupon tanımlanamadı: ${kupon.kod}`).toBeTruthy();
}

async function oturumAc(page: Page, baglam: Baglam): Promise<void> {
  await page.addInitScript((t) => window.localStorage.setItem('authToken', t), baglam.token);
}

// --- When: tek tetikleyici eylem ---
async function kuponUygula(page: Page, kod: string): Promise<void> {
  await page.getByLabel('İndirim kuponu').fill(kod);
  await page.getByRole('button', { name: 'Kuponu uygula' }).click();
}

test('@HIK-412 geçerli yüzdelik kupon sepet toplamına uygulanır', async ({ page, request }) => {
  // Given sepetinde 500 TL ürün bulunan bir kullanıcı vardır
  const baglam = await sepetiOlanKullanici(request, 500);
  // And BAHAR10 kuponu aktiftir ve %10 indirim tanımlıdır
  await kuponTanimla(request, { kod: 'BAHAR10', yuzde: 10, bitisTarihi: '2099-12-31' });
  await oturumAc(page, baglam);
  await page.goto(`/sepet/${baglam.sepetId}`);

  // When kullanıcı BAHAR10 kuponunu uygular
  await kuponUygula(page, 'BAHAR10');

  // Then sepet toplamı 450,00 TL olarak görünür
  await expect(page.getByTestId('sepet-toplam')).toHaveText('450,00 TL');
  // And kupon mesajı görünür
  await expect(page.getByTestId('kupon-mesaj')).toHaveText('Kupon uygulandı: %10 indirim');
});

Dikkat edilecek nokta: sepetiOlanKullanici her çağrıldığında kendi kullanıcısını ve sepetini üretir. Paylaşılan hesap kullanmak testleri birbirine bağlar; nedenleri test verisi yönetimi yazısında ayrıntılı.

Örnek tabloları ve parametrik senaryolar

“İndirim doğru hesaplanmalı” kriteri tek senaryo değildir; aynı kuralın farklı girdilerle tekrarıdır. Senaryoyu kopyalamak yerine örnek tablosu kullanın. Tablo hem iş analistinin okuyabileceği bir spesifikasyon hem de testin veri kaynağıdır. Bu hesaplama kuralları birim seviyesine aittir; aşağıdaki örnek, aynı tablonun API seviyesinde nasıl koştuğunu gösteriyor.

// tests/kupon-hesaplama.spec.ts
import { test, expect, type APIRequestContext } from '@playwright/test';

const API = process.env.API_URL ?? 'http://localhost:3000/api';

type Ornek = {
  aciklama: string;
  sepetToplami: number;
  kupon: { kod: string; yuzde: number; bitisTarihi: string; minimumTutar?: number };
  beklenenDurum: number;
  beklenenToplam?: number;
  beklenenHataKodu?: string;
};

const ornekler: Ornek[] = [
  { aciklama: '%10 kupon 500 TL sepette 450 TL bırakır',
    sepetToplami: 500,
    kupon: { kod: 'YUZDE10', yuzde: 10, bitisTarihi: '2099-12-31' },
    beklenenDurum: 200, beklenenToplam: 450 },
  { aciklama: 'kuruşlu tutar yukarı yuvarlanmaz',
    sepetToplami: 33.33,
    kupon: { kod: 'YUZDE15', yuzde: 15, bitisTarihi: '2099-12-31' },
    beklenenDurum: 200, beklenenToplam: 28.33 },
  { aciklama: 'süresi dolmuş kupon reddedilir',
    sepetToplami: 500,
    kupon: { kod: 'GECMIS20', yuzde: 20, bitisTarihi: '2020-01-01' },
    beklenenDurum: 422, beklenenHataKodu: 'KUPON_SURESI_DOLDU' },
  { aciklama: 'minimum tutarın altındaki sepette kupon reddedilir',
    sepetToplami: 80,
    kupon: { kod: 'MIN100', yuzde: 10, bitisTarihi: '2099-12-31', minimumTutar: 100 },
    beklenenDurum: 422, beklenenHataKodu: 'MINIMUM_TUTAR_ALTINDA' },
];

async function sepetOlustur(request: APIRequestContext, tutar: number) {
  const kayit = await request.post(`${API}/test/kullanicilar`, {
    data: { eposta: `hesap-${Date.now()}-${Math.random().toString(16).slice(2)}@ornek.test`,
            sifre: 'Test1234!' },
  });
  expect(kayit.ok()).toBeTruthy();
  const { token } = await kayit.json();

  const sepet = await request.post(`${API}/sepetler`, {
    headers: { Authorization: `Bearer ${token}` },
    data: { urunler: [{ sku: 'TEST-SKU-1', adet: 1, birimFiyat: tutar }] },
  });
  expect(sepet.ok()).toBeTruthy();
  const { id } = await sepet.json();
  return { id, token };
}

for (const ornek of ornekler) {
  test(`@HIK-412 kupon hesaplama: ${ornek.aciklama}`, async ({ request }) => {
    // Given
    const sepet = await sepetOlustur(request, ornek.sepetToplami);
    const kuponCevap = await request.post(`${API}/test/kuponlar`, { data: ornek.kupon });
    expect(kuponCevap.ok()).toBeTruthy();

    // When
    const cevap = await request.post(`${API}/sepetler/${sepet.id}/kupon`, {
      headers: { Authorization: `Bearer ${sepet.token}` },
      data: { kod: ornek.kupon.kod },
    });

    // Then
    expect(cevap.status()).toBe(ornek.beklenenDurum);
    const govde = await cevap.json();
    if (ornek.beklenenToplam !== undefined) {
      expect(govde.odenecekTutar).toBeCloseTo(ornek.beklenenToplam, 2);
    }
    if (ornek.beklenenHataKodu !== undefined) {
      expect(govde.hataKodu).toBe(ornek.beklenenHataKodu);
    }
  });
}

Her satır ayrı test adıyla raporlanır; düşen satırın hangi kural olduğu rapordan okunur. Tablo büyüdüğünde ayıklama ölçütü şudur: bir satır, kuralın başka bir dalını tetiklemiyorsa gereksizdir. %10 ve %15 indirim aynı formülü koşuyorsa biri yeter; sınır değerler (minimum tutarın tam üstü ve tam altı, bitiş tarihinin son günü) ise asla silinmez.

Kriter ile test arasında izlenebilirlik kurmak

İzlenebilirlik için ayrı bir matris tablosu tutmaya gerek yok. Elle bakımı yapılan matrisler iki sprint sonra gerçekle uyuşmaz hale gelir; test adı ve etiket ise kodla birlikte yaşar.

İki kural yeter: test başlığı kriterin cümlesini içersin, hikâye veya kriter kimliği etiket olarak başa eklensin. Yukarıdaki örneklerde @HIK-412 tam olarak bunu yapıyor. Sonuçları:

  • Bir kriter değiştiğinde hangi testlerin güncelleneceği grep -r "@HIK-412" tests/ ile bulunur.
  • Yalnızca o hikâyenin testleri koşulabilir: npx playwright test --grep @HIK-412.
  • Bir test düştüğünde rapordaki başlık, bozulan iş kuralını cümle olarak verir. “sepet.spec.ts:42 failed” ile “kupon hesaplama: minimum tutarın altındaki sepette kupon reddedilir” arasındaki fark, hata ayıklamaya harcanan yarım saattir.
  • Regresyon seti daraltılırken riskli alanı etikete göre seçebilirsiniz.

Kriterin yazıldığı yer ekibe göre değişir: Jira açıklaması, ayrı .feature dosyası veya test yönetim aracı. Üçünün de aynı zayıf noktası var: kriter kodun yaşadığı yerden uzaklaştıkça güncellenmez. Bu nedenle tek kaynak seçin ve testin adını o kaynaktaki cümleden türetin; kopyası nerede dururca dursun, güncel olanın hangisi olduğu tartışılmasın.

Gherkin/Cucumber katmanı da benzer bir karardır. Ayrı .feature dosyaları, senaryoları iş tarafının düzenleyebileceği bir biçimde tutar; karşılığında adım tanımları (step definitions) adlı ikinci bir soyutlama katmanının bakımını üstlenirsiniz. İş tarafı senaryoları gerçekten okuyup değiştirmiyorsa bu katman net bir maliyettir; GWT’yi düşünme çerçevesi olarak tutup testleri doğrudan Playwright’ta yazmak aynı netliği daha az kodla verir. Ölçüt niyet değil davranıştır: son üç ayda .feature dosyalarını geliştirici olmayan biri değiştirdi mi?

Kriteri yazarken yapılan ritüel: üç taraf, on beş dakika

Kabul kriteri tek kişinin masasında yazıldığında muğlak kalır, çünkü yazan kişi kendi kafasındaki boşluğu göremez. Boşluğu ancak aynı cümleyi farklı okuyan biri gösterir. Bu yüzden ürün sahibi, geliştirici ve testçi refinement sırasında senaryoyu birlikte yazmalı. On beş dakika, bir hikâye için fazlasıyla yeterlidir; amaç tüm senaryoları üretmek değil, yorum farkını kod yazılmadan önce ortadan kaldırmak.

Toplantının somut çıktısı üç şeydir: GWT formatında yazılmış kriterler, her kriterin hangi seviyede koşacağına dair karar ve “test edilmeyecek” diye açıkça işaretlenmiş maddeler. Üçüncüsü en çok atlanan ve en çok işe yarayandır; kapsam dışı olduğu yazılmamış her madde, sprint sonunda tartışma konusudur.

Kontrol listesi:

  • [ ] Her kriterin gözlemlenebilir bir çıktısı var mı?
  • [ ] Başlangıç durumu tek cümleyle yazılabiliyor mu?
  • [ ] Senaryoda tek bir When var mı?
  • [ ] Sonuç doğru/yanlış diye cevaplanabiliyor mu?
  • [ ] Tam mesaj metni, durum kodu veya ekran adı yazılı mı?
  • [ ] Sınır durumları soruldu mu (boş, sıfır, süresi dolmuş, yetkisiz)?
  • [ ] Her kriterin koşacağı seviye (birim / API / E2E) işaretli mi?
  • [ ] Kapsam dışı bırakılanlar açıkça yazıldı mı?
  • [ ] Kriterlere hikâye kimliği etiketi verildi mi?
  • [ ] Test verisini kim üretecek, belli mi?

Kaçınılması gereken kalıplar

UI adımlarını senaryoya yazmak. “When kullanıcı menüden Sepetim’e tıklar, kupon alanını doldurur, Uygula butonuna basar” — bu senaryo arayüz değiştiğinde kurala hiç dokunulmamış olsa bile çöker. Düzeltilmiş hali: “When kullanıcı sepetine BAHAR10 kuponunu uygular.” Hangi butona tıklanacağı test kodunun detayıdır, kriterin değil.

Tek senaryoda birden fazla iş kuralı doğrulamak. Hem indirim tutarını hem kargo bedelini hem stok düşümünü aynı senaryoda kontrol eden test düştüğünde hangi kuralın bozulduğu belli olmaz ve ilk expect düşünce kalanlar hiç çalışmaz. Her kural kendi senaryosuna.

Senaryolar arasında paylaşılan veri veya durum. “Önceki testte oluşturulan sipariş” ile çalışan bir senaryo, paralel koşumda ve tek başına koşturulduğunda farklı davranır; bu, flaky testlerin en sık kaynaklarından biridir (teşhis yöntemleri). Düzeltilmiş hali: her senaryo kendi Given’ını kendi üretir.

Negatif ve ölçülemez sonuç. “Then sistem hata vermemeli” cümlesi ikili değildir, çünkü sonsuz sayıda “hata vermeme” hali vardır ve test hiçbir şey doğrulamadan geçer. Düzeltilmiş hali: “Then HTTP 200 döner ve cevap gövdesinde odenecekTutar 450.00 olur.”

Bir sonraki hikâyede ne yapacaksın

  1. Açık sprintteki bir hikâyeyi aç, kriterlerini yukarıdaki dört şarta göre tek tek denetle.
  2. En muğlak olanı seç ve GWT’ye çevir; tam mesaj metnini veya durum kodunu yazmak zorunda kalana kadar soru sormayı bırakma.
  3. O kriterin hangi seviyede (birim / API / E2E) koşacağına karar ver ve kararı kriterin yanına yaz.
  4. Tek bir çalışan test yaz, adına hikâye kimliğini etiket olarak ekle.
  5. Kontrol listesini bir sonraki refinement’a bas ve ekranda açık tut.

İlk hafta ölçülecek tek şey var: kriter başına gelen yorum ve soru sayısı düştü mü? Düşmediyse kriterler hâlâ muğlak, şablonu doldurmuş olmanız bunu değiştirmez.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site istenmeyenleri azaltmak için Akismet kullanır. Yorum verilerinizin nasıl işlendiğini öğrenin.