Ders 12 / 13
Kabul Testi Odaklı Geliştirme
Bir kullanıcı isteğinin kabul ölçütlerinin adım biçiminde yazılması, adımları uygulamaya bağlayan bir sürücüyle otomatikleştirilmesi ve dış halkası kabul ölçütünde iç halkası birim testinde dönen iki katmanlı döngü.
İçindekiler
Önceki derste örnekler tek bir işlevin sözleşmesini anlattı. Bir kullanıcı isteği ise bundan geniştir. “Üye kitabının süresini kendi uzatabilsin” cümlesi bir işlevi değil bir akışı tarif eder: kayıt bulunur, kural uygulanır, sonuç kalıcı hâle gelir ve üyeye haber verilir. Bu akışın tamamlandığını söyleyen ölçütler de isteğin kendisiyle birlikte gelir.
Kabul testi odaklı geliştirme (acceptance test driven development) o ölçütleri geliştirmenin başlangıç noktası yapar. Ölçüt önce yazılır, otomatik bir denetime çevrilir ve kırmızı kalır; işin bittiğini söyleyen şey o denetimin yeşile dönmesidir. Birim testleri ortadan kalkmaz — kabul ölçütünün gösterdiği yere yazılırlar.
Kabul Ölçütünün Biçimi
Ölçüt üç tür adımdan oluşur. Verilen adımları başlangıç durumunu kurar, yapıldığında adımı tek bir işlemi tetikler, beklenen adımları sonucu denetler. Bu ayrım birinci dersteki hazırla, uygula, doğrula bölümlerinin akış düzeyindeki karşılığıdır.
cat > kabul.txt <<'KABUL' # kabul olcutleri — uye kendi oduncunun suresini uzatabilsin hikaye: Uye elindeki kitabin suresini kendi uzatabilsin olcut: Uygun bir odunc yenilendiginde son tarih ileri alinir ve uyeye bildirim gider verilen: uye no=U-17 tur=ogrenci verilen: odunc kitapNo=K-903 uyeNo=U-17 sonGun=1028 yapildiginda: yenileme uyeNo=U-17 kitapNo=K-903 bugun=1020 beklenen: odunc kitapNo=K-903 sonGun=1048 yenilemeSayisi=1 beklenen: bildirim uyeNo=U-17 ileti="yeni son tarih: 1048" olcut: Rezerve edilmis kitabin yenilenmesi reddedilir ve son tarih degismez verilen: uye no=U-42 tur=uye verilen: odunc kitapNo=K-101 uyeNo=U-42 sonGun=1024 rezerveEdildi=evet yapildiginda: yenileme uyeNo=U-42 kitapNo=K-101 bugun=1020 beklenen: red gerekce="rezerve edilmis kitap yenilenemez" beklenen: odunc kitapNo=K-101 sonGun=1024 yenilemeSayisi=0 KABUL
Önceki dersteki örnek satırlarından iki fark var. Adımlar sıralıdır ve durumu paylaşır: ölçüt bir senaryo anlatır, tek bir çağrı değil. İkincisi, adımlarda geçen sözcükler uygulamanın kavramlarıdır — üye, ödünç, bildirim, red — belirli bir işlevin imzası değil.
// kabul-cozumleyici.mjs — kabul olcutlerini adim listesine cevirir const ALAN = /(\w+)=("[^"]*"|\S+)/g; const ADIM_ANAHTARLARI = ['verilen', 'yapildiginda', 'beklenen']; const cevir = (ham) => { const deger = ham.startsWith('"') ? ham.slice(1, -1) : ham; if (deger === 'evet') return true; if (deger === 'hayir') return false; return /^-?\d+$/.test(deger) ? Number(deger) : deger; }; const alanlar = (metin) => Object.fromEntries( [...metin.matchAll(ALAN)].map(([, ad, ham]) => [ad, cevir(ham)]), ); export function cozumle(metin) { const olcutler = []; for (const ham of metin.split('\n')) { const satir = ham.trim(); if (satir === '' || satir.startsWith('#') || satir.startsWith('hikaye:')) continue; if (satir.startsWith('olcut:')) { olcutler.push({ baslik: satir.slice(6).trim(), adimlar: [] }); continue; } const [anahtar, ...kalan] = satir.split(':'); if (ADIM_ANAHTARLARI.includes(anahtar) === false) throw new Error(`taninmayan adim: ${satir}`); if (olcutler.length === 0) throw new Error(`olcutsuz adim: ${satir}`); const govde = kalan.join(':').trim(); olcutler.at(-1).adimlar.push({ ad: `${anahtar} ${govde.split(/\s+/)[0]}`, alanlar: alanlar(govde), metin: satir, }); } return olcutler; }
Uygulama ve Sürücü
Ölçütlerin dokunduğu birim tek bir kural işlevi değil, akışı yürüten servistir. Servis kaydı yükler, kuralı uygular ve sonucu kaydeder. Bu ilk sürümde bildirim gönderimi yoktur; ölçütün kırmızı kalacağı yer orasıdır.
// yenileme.mjs — yenileme kurallari export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } }; export const EN_COK_YENILEME = 2; export function yenile(kayit, bugun) { if (kayit.rezerveEdildi) throw new Error('rezerve edilmis kitap yenilenemez'); if (kayit.yenilemeSayisi >= EN_COK_YENILEME) throw new Error('en cok 2 kez yenilenebilir'); if (bugun > kayit.sonGun) throw new Error('gecikmis odunc yenilenemez'); return { ...kayit, sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun, yenilemeSayisi: kayit.yenilemeSayisi + 1, }; }
// servis.mjs — surum 1: kaydi yukler, kurali uygular, sonucu kaydeder import { yenile } from './yenileme.mjs'; export function servisKur({ depo, bildirim }) { return { yenile(uyeNo, kitapNo, bugun) { const uye = depo.uyeBul(uyeNo); const odunc = depo.oduncBul(kitapNo); if (uye === undefined || odunc === undefined || odunc.uyeNo !== uyeNo) { return { durum: 'red', gerekce: 'odunc bulunamadi' }; } try { const yeni = yenile({ ...odunc, uyeTuru: uye.tur }, bugun); depo.oduncKaydet(yeni); return { durum: 'kabul', odunc: yeni }; } catch (hata) { return { durum: 'red', gerekce: hata.message }; } }, }; }
Metin ile uygulama arasındaki bağ sürücüdür (driver): her adım türünü bir kod parçasına eşleyen tablo. Sürücü, altıncı derste tanıtılan taklit nesnelerle bir “dünya” kurar ve adımları o dünya üzerinde yürütür.
// kabul.test.mjs — kabul olcutlerini surucuyle kosulabilir teste cevirir import { readFileSync } from 'node:fs'; import { describe, test } from 'node:test'; import assert from 'node:assert/strict'; import { cozumle } from './kabul-cozumleyici.mjs'; import { servisKur } from './servis.mjs'; function dunyaKur() { const uyeler = new Map(); const oduncler = new Map(); const bildirimler = []; const depo = { uyeEkle: (uye) => uyeler.set(uye.no, uye), uyeBul: (no) => uyeler.get(no), oduncBul: (kitapNo) => oduncler.get(kitapNo), oduncKaydet: (odunc) => oduncler.set(odunc.kitapNo, odunc), }; const bildirim = { gonder: (uyeNo, ileti) => bildirimler.push([uyeNo, ileti]) }; return { depo, bildirimler, servis: servisKur({ depo, bildirim }), sonuc: undefined }; } const VARSAYILAN_ODUNC = { yenilemeSayisi: 0, rezerveEdildi: false }; const SURUCU = { 'verilen uye': (d, a) => d.depo.uyeEkle({ no: a.no, tur: a.tur }), 'verilen odunc': (d, a) => d.depo.oduncKaydet({ ...VARSAYILAN_ODUNC, ...a }), 'yapildiginda yenileme': (d, a) => { d.sonuc = d.servis.yenile(a.uyeNo, a.kitapNo, a.bugun); }, 'beklenen odunc': (d, a) => { const { kitapNo, ...beklenen } = a; const kayit = d.depo.oduncBul(kitapNo); for (const [alan, deger] of Object.entries(beklenen)) { assert.equal(kayit[alan], deger, `${kitapNo}.${alan} uyusmuyor`); } }, 'beklenen bildirim': (d, a) => assert.deepEqual(d.bildirimler, [[a.uyeNo, a.ileti]]), 'beklenen red': (d, a) => { assert.equal(d.sonuc.durum, 'red'); assert.equal(d.sonuc.gerekce, a.gerekce); }, }; const metin = readFileSync(new URL('./kabul.txt', import.meta.url), 'utf8'); for (const olcut of cozumle(metin)) { describe(olcut.baslik, () => { const dunya = dunyaKur(); for (const adim of olcut.adimlar) { test(adim.metin, () => { const surucu = SURUCU[adim.ad]; if (surucu === undefined) throw new Error(`surucusu olmayan adim: ${adim.ad}`); surucu(dunya, adim.alanlar); }); } }); }
Her adım ayrı bir test olarak koşar ve aynı dünyayı paylaşır. Dördüncü derste kurulan bağımsızlık kuralı burada ölçüt düzeyinde uygulanır: paylaşım ölçüt içinde kalır, her ölçüt kendi dünyasını kurar.
Dış Halka: Kırmızı Ölçüt
node --test --test-reporter=tap kabul.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - verilen: uye no=U-17 tur=ogrenci
ok 2 - verilen: odunc kitapNo=K-903 uyeNo=U-17 sonGun=1028
ok 3 - yapildiginda: yenileme uyeNo=U-17 kitapNo=K-903 bugun=1020
ok 4 - beklenen: odunc kitapNo=K-903 sonGun=1048 yenilemeSayisi=1
not ok 5 - beklenen: bildirim uyeNo=U-17 ileti="yeni son tarih: 1048"
not ok 1 - Uygun bir odunc yenilendiginde son tarih ileri alinir ve uyeye bildirim gider
ok 1 - verilen: uye no=U-42 tur=uye
ok 2 - verilen: odunc kitapNo=K-101 uyeNo=U-42 sonGun=1024 rezerveEdildi=evet
ok 3 - yapildiginda: yenileme uyeNo=U-42 kitapNo=K-101 bugun=1020
ok 4 - beklenen: red gerekce="rezerve edilmis kitap yenilenemez"
ok 5 - beklenen: odunc kitapNo=K-101 sonGun=1024 yenilemeSayisi=0
ok 2 - Rezerve edilmis kitabin yenilenmesi reddedilir ve son tarih degismez
# tests 10
# pass 9
# fail 1
İkinci ölçüt karşılanıyor, birincisi karşılanmıyor. Daha değerlisi, hangi adımda karşılanmadığı görünüyor: son tarih doğru ilerledi, kayıt doğru güncellendi, eksik olan tek şey bildirim. Kabul ölçütü böylece bir kırmızı adım değil, kırmızı adımın adresi oldu.
İç Halka: Birim Testi
Kabul ölçütü doğrudan gerçekleştirime çevrilmez. İşaret ettiği yerde bir birim testi yazılır ve döngü orada döner. Bu iki katmanlı yapıya dış halka ve iç halka denir: dış halka bir kullanıcı isteği boyunca kırmızı kalır, iç halka o süre içinde birçok kez kırmızıdan yeşile geçer.
// servis.test.mjs — ic halka: kabul olcutunun isaret ettigi birim import { test } from 'node:test'; import assert from 'node:assert/strict'; import { servisKur } from './servis.mjs'; function kur() { const oduncler = new Map([['K-903', { kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1028, yenilemeSayisi: 0, rezerveEdildi: false }]]); const bildirimler = []; const depo = { uyeBul: () => ({ no: 'U-17', tur: 'ogrenci' }), oduncBul: (kitapNo) => oduncler.get(kitapNo), oduncKaydet: (odunc) => oduncler.set(odunc.kitapNo, odunc), }; return { servis: servisKur({ depo, bildirim: { gonder: (u, i) => bildirimler.push([u, i]) } }), bildirimler }; } test('kabul edilen yenileme uyeye yeni son tarihi bildirir', () => { const { servis, bildirimler } = kur(); servis.yenile('U-17', 'K-903', 1020); assert.deepEqual(bildirimler, [['U-17', 'yeni son tarih: 1048']]); });
node --test --test-reporter=tap servis.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
not ok 1 - kabul edilen yenileme uyeye yeni son tarihi bildirir # tests 1 # pass 0 # fail 1
Yeşil adım tek satırdır.
// servis.mjs — surum 2: kabul edilen yenilemede uyeye bildirim gonderilir import { yenile } from './yenileme.mjs'; export function servisKur({ depo, bildirim }) { return { yenile(uyeNo, kitapNo, bugun) { const uye = depo.uyeBul(uyeNo); const odunc = depo.oduncBul(kitapNo); if (uye === undefined || odunc === undefined || odunc.uyeNo !== uyeNo) { return { durum: 'red', gerekce: 'odunc bulunamadi' }; } try { const yeni = yenile({ ...odunc, uyeTuru: uye.tur }, bugun); depo.oduncKaydet(yeni); bildirim.gonder(uyeNo, `yeni son tarih: ${yeni.sonGun}`); return { durum: 'kabul', odunc: yeni }; } catch (hata) { return { durum: 'red', gerekce: hata.message }; } }, }; }
node --test --test-reporter=tap servis.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))' node --test --test-reporter=tap kabul.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
ok 1 - kabul edilen yenileme uyeye yeni son tarihi bildirir # tests 1 # pass 1 # fail 0 ok 1 - Uygun bir odunc yenilendiginde son tarih ileri alinir ve uyeye bildirim gider ok 2 - Rezerve edilmis kitabin yenilenmesi reddedilir ve son tarih degismez # tests 10 # pass 10 # fail 0
İç halka yeşile döndü ve dış halka onunla birlikte kapandı. İkinci komutta yalnız girintisiz satırlar süzülüyor; ölçüt düzeyinde bakıldığında iki satır, işin bittiğini söyleyen bütün bilgidir.
İki Halkanın İş Bölümü
Dış ve iç halkanın farkı hız ve kapsamdadır ve bu fark bilerek korunur.
Dış halka yavaş ve geniştir. Bir ölçüt akışın tamamına dokunur, bu yüzden düştüğünde nerede bozulduğunu tek başına söylemez — yukarıdaki koşumda adım adım raporlama bu eksiği kısmen kapatıyor. Sayısı azdır: bir kullanıcı isteği başına birkaç ölçüt.
İç halka hızlı ve dardır. Bir birim testi tek bir kuralı sınar ve düştüğünde adresi verir. Sayısı çoktur ve ikinci derste ölçülen sinyal gücü orada aranır.
İki halkanın karıştırılması iki tipik hataya yol açar. Kabul ölçütleri birim testi gibi çoğaltıldığında koşum yavaşlar ve ölçüt listesi okunamaz hâle gelir; kullanıcıyla paylaşılabilir olma özelliği kaybolur. Ters yönde, birim testleri hiç yazılmayıp her şey kabul ölçütüne bırakıldığında düşen bir ölçüt uzun bir arama başlatır — ikinci dersteki arama alanı ölçümü burada da geçerlidir.
Kabul ölçütünün bittiğini söyleyen şey de bellidir: ölçüt yeşile döndüğünde iş kabul edilmiş sayılır. Bu, “geliştirici bitti dedi” ölçütünden farklıdır, çünkü bitmişliğin tanımı işi isteyen tarafla birlikte ve önceden yazılmıştır.
Özet
- Kabul ölçütü, bir kullanıcı isteğinin karşılandığını söyleyen ve önceden yazılan denetimdir; verilen, yapıldığında ve beklenen adımlarından oluşur.
- Ölçüt adımları sıralıdır ve durumu paylaşır; paylaşım ölçüt içinde kalır, her ölçüt kendi dünyasını kurar.
- Sürücü, metindeki adım türlerini uygulama çağrılarına eşler; sürücüsü olmayan adım sessizce atlanmaz, hata verir.
- Kırmızı bir kabul ölçütü kırmızı adımın kendisi değil adresidir; iç halkada yazılan birim testi o adrese gider.
- Dış halka yavaş, geniş ve az sayıdadır; iç halka hızlı, dar ve çok sayıdadır. İkisinin karıştırılması ya okunamaz bir ölçüt listesi ya da uzun bir arama alanı üretir.
Sonraki Adım
Bu konuda kurulan her şey boş bir sayfadan başladı: test önce yazıldı, kod sonra geldi. Var olan bir kod tabanında bu sıra kurulamaz. Yıllardır çalışan, testi olmayan ve bağımlılıklarını kendi içinde yaratan bir işlevin önce ne yaptığı kayda geçirilmeli, sonra değiştirilebilir hâle getirilmelidir. Sonraki ders bu işi üç aşamada yapar ve kursu kapatır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.