Ders 11 / 13
Davranış Odaklı Geliştirme
Kuralların alan diliyle yazılmış örnek tablolarıyla tanımlanması, tablo için bir biçim ve çözümleyici yazılması ve tablodan üretilen veri güdümlü testlerle döngünün sürdürülmesi.
İçindekiler
Önceki dersteki yedi test, kuralları ayrı ayrı cümlelerle anlattı ve her cümle bir kod bloğu oldu. Kurallar çoğaldıkça bu yazım tekrar üretir: aynı düzenek, aynı çağrı, yalnız girdi ve beklenen sonuç değişir. Daha önemlisi, kuralın kendisi koda gömülü kalır. Ödünç kurallarını belirleyen kişi kütüphane görevlisidir, ama kuralların yazılı olduğu yer bir test dosyasıdır ve o dosya görevliyle konuşurken açılamaz.
Davranış odaklı geliştirme (behavior driven development) bu boşluğu kapatmayı hedefler. Döngü aynı döngüdür — kırmızı, yeşil, yeniden düzenle — ama konuşmanın birimi bir test işlevi değil, alan diliyle yazılmış bir örnektir. Örnek üç parça taşır: verilen bir başlangıç durumu, yapılan bir işlem ve beklenen sonuç. Bu üçlü, birinci dersteki hazırla, uygula ve doğrula bölümlerinin alan diline çevrilmiş hâlidir.
Örnek Tablosunun Biçimi
Örneklerin yazılacağı biçim bir standarda bağlı değildir; taşıması gereken tek şey, hem görevlinin okuyabilmesi hem de bir programın ayrıştırabilmesidir. Aşağıdaki biçim iki satır türü tanır: bir kural başlığı ve o kurala ait örnekler.
cat > ornekler.txt <<'ORNEK' # yenileme sozlesmesi — her kural, kendisini gosteren orneklerle tanimlidir kural: Yenileme son tarihi bugunden itibaren uye turunun odunc suresi kadar ileri alir ornek: uyeTuru=ogrenci bugun=1020 sonGun=1028 -> sonGun=1048 ornek: uyeTuru=uye bugun=1020 sonGun=1024 -> sonGun=1034 kural: Rezerve edilmis kitap yenilenemez ornek: bugun=1020 sonGun=1028 rezerveEdildi=evet -> hata=rezerve edilmis kural: Bir odunc en cok iki kez yenilenebilir ornek: bugun=1020 sonGun=1028 yenilemeSayisi=1 -> yenilemeSayisi=2 ornek: bugun=1020 sonGun=1028 yenilemeSayisi=2 -> hata=en cok 2 kez kural: Gecikmis odunc yenilenemez, son gunde yapilan yenileme kabul edilir ornek: bugun=1029 sonGun=1028 -> hata=gecikmis odunc ornek: bugun=1028 sonGun=1028 -> sonGun=1056 ORNEK
Ok işaretinin solu verilen durumdur, sağı beklenen sonuçtur; yapılan işlem tektir ve dosyanın adından bellidir. Belirtilmeyen alanlar varsayılan değerlerini alır, böylece her örnek yalnız kendi kuralıyla ilgili alanları gösterir.
Çözümleyici
Biçim kendi kendine çalışmaz; onu veri yapısına çeviren bir çözümleyici gerekir. Elli satırın altında bir çözümleyici bu iş için yeterlidir ve tanımadığı satırda susmak yerine hata vermesi önemlidir — sessizce atlanan bir satır, hiç koşmayan bir kural demektir.
// cozumleyici.mjs — ornek dosyasini kural ve ornek listesine cevirir const deger = (metin) => { if (metin === 'evet') return true; if (metin === 'hayir') return false; return /^-?\d+$/.test(metin) ? Number(metin) : metin; }; const alanlar = (parca) => Object.fromEntries( parca.trim().split(/\s+/).filter(Boolean).map((ikili) => { const [ad, ...kalan] = ikili.split('='); return [ad, deger(kalan.join('='))]; }), ); export function cozumle(metin) { const kurallar = []; for (const ham of metin.split('\n')) { const satir = ham.trim(); if (satir === '' || satir.startsWith('#')) continue; if (satir.startsWith('kural:')) { kurallar.push({ baslik: satir.slice(6).trim(), ornekler: [] }); continue; } if (satir.startsWith('ornek:')) { if (kurallar.length === 0) throw new Error(`kuralsiz ornek: ${satir}`); const govde = satir.slice(6); const [girdi, cikti] = govde.split('->'); if (cikti === undefined) throw new Error(`ok isareti eksik: ${satir}`); const beklenen = cikti.trim(); kurallar.at(-1).ornekler.push({ metin: govde.trim(), girdi: alanlar(girdi), hata: beklenen.startsWith('hata=') ? beklenen.slice(5) : undefined, sonuc: beklenen.startsWith('hata=') ? undefined : alanlar(beklenen), }); continue; } throw new Error(`taninmayan satir: ${satir}`); } return kurallar; }
Sınanacak kitaplık, önceki dersin dört kuralını uygulayan sürümdür.
// yenileme.mjs — dort kuralin tamami export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } }; export const EN_COK_YENILEME = 2; export const SINIR_ILETISI = `en cok ${EN_COK_YENILEME} kez yenilenebilir`; export function yenile(kayit, bugun) { if (kayit.rezerveEdildi) throw new Error('rezerve edilmis kitap yenilenemez'); if (kayit.yenilemeSayisi >= EN_COK_YENILEME) throw new Error(SINIR_ILETISI); if (bugun > kayit.sonGun) throw new Error('gecikmis odunc yenilenemez'); return { ...kayit, sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun, yenilemeSayisi: kayit.yenilemeSayisi + 1, }; }
Tablodan Üretilen Testler
Testler artık elle yazılmaz; çözümlenmiş tablodan üretilir. Bu yazım biçimine veri güdümlü test (data driven test) denir: test gövdesi tektir, girdi kümesi veriden gelir.
// ornek.test.mjs — ornek tablosundan uretilen veri gudumlu testler import { readFileSync } from 'node:fs'; import { describe, test } from 'node:test'; import assert from 'node:assert/strict'; import { cozumle } from './cozumleyici.mjs'; import { yenile } from './yenileme.mjs'; const VARSAYILAN = { uyeNo: 'U-17', uyeTuru: 'ogrenci', sonGun: 1028, yenilemeSayisi: 0, rezerveEdildi: false, siradakiUye: 'U-99', }; const metin = readFileSync(new URL('./ornekler.txt', import.meta.url), 'utf8'); for (const kural of cozumle(metin)) { describe(kural.baslik, () => { for (const ornek of kural.ornekler) { test(ornek.metin, () => { const { bugun, ...ustuneYaz } = ornek.girdi; const kayit = { ...VARSAYILAN, ...ustuneYaz }; if (ornek.hata !== undefined) { assert.throws(() => yenile(kayit, bugun), new RegExp(ornek.hata)); return; } const yeni = yenile(kayit, bugun); for (const [alan, beklenen] of Object.entries(ornek.sonuc)) { assert.equal(yeni[alan], beklenen, `${alan} uyusmuyor`); } }); } }); }
node --test --test-reporter=tap ornek.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - uyeTuru=ogrenci bugun=1020 sonGun=1028 -> sonGun=1048
ok 2 - uyeTuru=uye bugun=1020 sonGun=1024 -> sonGun=1034
ok 1 - Yenileme son tarihi bugunden itibaren uye turunun odunc suresi kadar ileri alir
ok 1 - bugun=1020 sonGun=1028 rezerveEdildi=evet -> hata=rezerve edilmis
ok 2 - Rezerve edilmis kitap yenilenemez
ok 1 - bugun=1020 sonGun=1028 yenilemeSayisi=1 -> yenilemeSayisi=2
ok 2 - bugun=1020 sonGun=1028 yenilemeSayisi=2 -> hata=en cok 2 kez
ok 3 - Bir odunc en cok iki kez yenilenebilir
ok 1 - bugun=1029 sonGun=1028 -> hata=gecikmis odunc
ok 2 - bugun=1028 sonGun=1028 -> sonGun=1056
ok 4 - Gecikmis odunc yenilenemez, son gunde yapilan yenileme kabul edilir
# tests 7
# pass 7
# fail 0
Üçüncü dersteki gözlem burada en güçlü hâline ulaşıyor: geçen testlerin listesi bir kural belgesidir. Girintisiz satırlar kuralları, girintili satırlar o kuralı gösteren örnekleri verir. Bu çıktı ile örnek dosyası aynı metni taşır — biri kaynağın, diğeri koşumun ifadesidir.
Kırmızı Adım Artık Bir Metin Satırıdır
Görevliyle yapılan bir konuşmadan yeni bir kural çıktığını varsayalım: rezerve edilmiş bir kitap, rezervasyon sırasındaki ilk üye kitabı elinde tutan üyenin kendisiyse yenilenebilir. Bu kuralın kırmızı adımı artık kod yazmayı gerektirmez.
cat >> ornekler.txt <<'ORNEK' kural: Rezervasyon sirasindaki ilk uye oduncu elinde tutan uyeyse yenileme kabul edilir ornek: bugun=1020 sonGun=1028 rezerveEdildi=evet siradakiUye=U-17 -> sonGun=1048 ORNEK
node --test --test-reporter=tap ornek.test.mjs | grep -E '^ *(not ok|# (tests|pass|fail))'
not ok 1 - bugun=1020 sonGun=1028 rezerveEdildi=evet siradakiUye=U-17 -> sonGun=1048 not ok 5 - Rezervasyon sirasindaki ilk uye oduncu elinde tutan uyeyse yenileme kabul edilir # tests 8 # pass 7 # fail 1
Test dosyasına tek satır eklenmedi. Kırmızı adımı başlatan şey, alan diliyle yazılmış iki satırlık bir metindir. Yeşil adım ise yine koddadır.
// yenileme.mjs — surum 2: rezervasyon sirasindaki ilk uye kurali eklendi export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } }; export const EN_COK_YENILEME = 2; export const SINIR_ILETISI = `en cok ${EN_COK_YENILEME} kez yenilenebilir`; export function yenile(kayit, bugun) { if (kayit.rezerveEdildi && kayit.siradakiUye !== kayit.uyeNo) { throw new Error('rezerve edilmis kitap yenilenemez'); } if (kayit.yenilemeSayisi >= EN_COK_YENILEME) throw new Error(SINIR_ILETISI); if (bugun > kayit.sonGun) throw new Error('gecikmis odunc yenilenemez'); return { ...kayit, sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun, yenilemeSayisi: kayit.yenilemeSayisi + 1, }; }
node --test --test-reporter=tap ornek.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - uyeTuru=ogrenci bugun=1020 sonGun=1028 -> sonGun=1048
ok 2 - uyeTuru=uye bugun=1020 sonGun=1024 -> sonGun=1034
ok 1 - Yenileme son tarihi bugunden itibaren uye turunun odunc suresi kadar ileri alir
ok 1 - bugun=1020 sonGun=1028 rezerveEdildi=evet -> hata=rezerve edilmis
ok 2 - Rezerve edilmis kitap yenilenemez
ok 1 - bugun=1020 sonGun=1028 yenilemeSayisi=1 -> yenilemeSayisi=2
ok 2 - bugun=1020 sonGun=1028 yenilemeSayisi=2 -> hata=en cok 2 kez
ok 3 - Bir odunc en cok iki kez yenilenebilir
ok 1 - bugun=1029 sonGun=1028 -> hata=gecikmis odunc
ok 2 - bugun=1028 sonGun=1028 -> sonGun=1056
ok 4 - Gecikmis odunc yenilenemez, son gunde yapilan yenileme kabul edilir
ok 1 - bugun=1020 sonGun=1028 rezerveEdildi=evet siradakiUye=U-17 -> sonGun=1048
ok 5 - Rezervasyon sirasindaki ilk uye oduncu elinde tutan uyeyse yenileme kabul edilir
# tests 8
# pass 8
# fail 0
Eski rezervasyon örneği hâlâ yeşil, çünkü sıradaki üye varsayılan olarak başkasıdır. Yeni kural eskisini geçersiz kılmadı, daralttı — ve bunu doğrulayan şey, tabloda ikisinin de durmasıdır.
Örneklerin Sınırı
Örnek tablosunun iki kazancı vardır ve ikisi de sınırlıdır.
Birinci kazanç ortak dildir: kural, kodu okumayan bir kişinin doğrulayabileceği biçimde yazılıdır. Sınırı şudur ki tablo ancak konuşulan bir dille yazıldığı sürece bu işe yarar. Alan sözcüğü yerine sınıf adı, kural cümlesi yerine yöntem imzası geçtiğinde tablo yalnız daha zahmetli bir test dosyasına dönüşür.
İkinci kazanç, aynı kural için birden çok örneğin ucuzlamasıdır. Yeni bir sınır durumu bir satırdır. Sınırı şudur ki her davranış tabloya sığmaz: koşum sırasına, zamana ya da çok adımlı bir senaryoya bağlı davranışlar için tablo biçimi yetersiz kalır ve elle yazılmış test daha açık olur. İki biçim birbirinin yerine değil, yan yana kullanılır.
Üçüncü bir uyarı çözümleyicinin kendisiyle ilgilidir. Tablodan test üreten kod, testlerin
tamamının doğruluğunun bağlı olduğu tek noktadır. Sessizce satır atlayan ya da beklenen
sonucu yanlış çözen bir çözümleyici, ikinci dersteki assert.ok kadar zararlıdır: her şey
yeşil görünür. Bu yüzden çözümleyici tanımadığı satırda hata fırlatır ve kendisi de sınanır.
Özet
- Davranış odaklı geliştirme aynı döngüyü kullanır, ama konuşmanın birimi alan diliyle yazılmış bir örnektir: verilen durum, yapılan işlem, beklenen sonuç.
- Örnek biçimi bir standarda bağlı değildir; taşıması gereken tek şey hem insanın okuyabilmesi hem de programın ayrıştırabilmesidir.
- Veri güdümlü testte test gövdesi tektir ve girdi kümesi veriden gelir; geçen testlerin listesi doğrudan bir kural belgesi verir.
- Yeni bir kuralın kırmızı adımı tabloya eklenen iki satırdır; test dosyasına dokunulmaz.
- Tablo biçimi ortak dil ve ucuz sınır durumu sağlar, ama sıraya ya da zamana bağlı çok adımlı davranışlar için elle yazılmış test daha açıktır.
Sonraki Adım
Bu derste örnekler tek bir işlevin sözleşmesini anlattı: yenileme kuralları. Bir kullanıcı isteği ise bundan geniştir — “üye kitabının süresini uzatabilsin” cümlesi, bir işlevi değil uçtan uca bir akışı tarif eder ve kabul edildiğini söyleyen ölçütler taşır. Sonraki ders o ölçütleri otomatik bir denetime çevirir ve dış halkası kabul ölçütünde, iç halkası birim testinde dönen iki katmanlı bir döngü kurar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.