İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat