---
title: 'Birim Testi ve Test Odaklı Geliştirme'
source: 'https://academia.sh/tr/kurslar/birim-testi'
language: tr
updated: '2026-08-17T04:43:01+00:00'
license: 'CC BY-SA 4.0'
lessons: 13
outcomes:
    - 'Hızlı ve bağımsız birim testleri yazar'
    - 'Test ikizlerini doğru türde kullanır'
    - 'Test odaklı geliştirme döngüsünü uygular'
---

# Birim Testi ve Test Odaklı Geliştirme

Hızlı, bağımsız ve niyeti okunabilir birim testlerinin yazımı; iddia seçiminin sinyal gücü, bağımlılıkların dışarıdan verilmesi, sahte bağımlılık türleri, kapsam ölçümünün anlamı ve testin kodu yönlendirdiği geliştirme döngüsü.

## Birim Test Pratiği

Bir birim testinin üç bölümü, iddiaların sinyal gücü, niyeti anlatan test adları, sıra bağımlılığının kırılması, bağımlılıkların dışarıdan verilmesi, sahte bağımlılık türleri, satır ve dal kapsamının anlamı ve gerçekleştirime bağlanan testlerin maliyeti.

### Birim Testin Anatomisi

Source: https://academia.sh/tr/kurslar/birim-testi/birim-testin-anatomisi

Bir hata raporunun yeniden üretim adımlarının koşulabilir teste çevrilmesi, test gövdesinin hazırla-uygula-doğrula bölümleri ve beklenen değerin gerçekleştirimden bağımsız kaynaktan gelmesi.

Kalite ve Test Temelleri kursu, bir hatanın raporlanmasıyla, önceliklendirilmesiyle ve
düzeltmenin doğrulanmasıyla kapandı. O kurs testin **ne** olduğunu ve **hangi** testin
yazılacağını kurdu: denklik sınıfları, sınır değerler, test kâhini, hata kaydı. Elde
kalan şey bir belgeydi — insanın okuyup elle uyguladığı adımlar.

Bu kurs aynı adımların **nasıl** yazıldığını ele alır. Bir hata kaydındaki "yeniden üretim
adımları" alanı, aslında bir programın taslağıdır: belirli bir başlangıç durumu, tek bir
işlem ve gözlenen sonuç. Bu üçlü koşulabilir hâle getirildiğinde, hatanın bir daha geri
gelmediğini kimsenin elle denetlemesi gerekmez. Bu dersin sorusu şudur: o çevirme işlemi
tam olarak neye benzer ve testin gövdesi hangi bölümlerden oluşur?

#### Yeniden Üretim Adımları

Kurs boyunca aynı alan sürer: kütüphane ödünç kurallarını uygulayan küçük bir kitaplık.
Kurallar yazılıdır. Öğrenci üyeler bir kitabı yirmi sekiz gün, üyeler on dört gün ödünç
alır. Geç iadede günlük ücret alınır, ilk birkaç gün ücretsizdir ve toplam ücretin bir üst
sınırı vardır. Tarihler gün numarasıyla temsil edilir; takvim aritmetiği bu kursun konusu
değildir ve sabit bir gün sayacı örnekleri belirlenimci tutar.

```js
// kitaplik.mjs — surum 1
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export const GECIKME = { ucretsizGun: 3, gunlukUcret: 2, ustSinir: 40 };

export function sonTarih(alisGunu, uyeTuru) {
  const kural = KURALLAR[uyeTuru];
  if (kural === undefined) throw new Error(`bilinmeyen uye turu: ${uyeTuru}`);
  return alisGunu + kural.oduncGun;
}

export function gecikmeUcreti(sonGun, iadeGunu) {
  const gecikme = iadeGunu - sonGun;
  if (gecikme <= GECIKME.ucretsizGun) return 0;
  return (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
}
```

Kütüphane görevlisinden gelen hata kaydı şunu söylüyor: bin numaralı günde kitap alan bir
üye, son tarihten kırk gün sonra iade ettiğinde yetmiş dört birim ücret hesaplanıyor; oysa
yazılı kural üst sınırı kırk olarak veriyor. Kaydın yeniden üretim adımları üç satırdır ve
her satır bir program ifadesine karşılık gelir.

```js
// yeniden-uretim.mjs — hata raporundaki adimlarin elle kosumu
import { sonTarih, gecikmeUcreti } from './kitaplik.mjs';

const son = sonTarih(1000, 'uye');
console.log(`son tarih       : ${son}`);
console.log(`iade gunu       : ${son + 40}`);
console.log(`hesaplanan ucret: ${gecikmeUcreti(son, son + 40)}`);
console.log(`beklenen ust sinir: 40`);
```

```
son tarih       : 1014
iade gunu       : 1054
hesaplanan ucret: 74
beklenen ust sinir: 40
```

Bu betik hatayı yeniden üretir ama bir test değildir. Sonuca insan bakar; kırk ile yetmiş
dördü karşılaştıran bir göz gerekir. Betiği teste çeviren şey, karşılaştırmanın da
programa girmesidir.

#### Hazırla, Uygula, Doğrula

Bir birim testinin gövdesi üç bölüme ayrılır ve bu bölümler her testte aynı sırayla gelir.

**Hazırla** (arrange) bölümü, işlemin çalışacağı başlangıç durumunu kurar: girdiler,
nesneler, bağımlılıklar. **Uygula** (act) bölümü sınanan işlemi **tek bir kez** çağırır.
**Doğrula** (assert) bölümü gözlenen sonucu beklenenle karşılaştırır ve uyuşmazlıkta testi
düşürür.

```js
// gecikme.test.mjs — hata raporunun yeniden uretim adimlari
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { sonTarih, gecikmeUcreti } from './kitaplik.mjs';

test('gecikme ucreti ust siniri asmaz', () => {
  const son = sonTarih(1000, 'uye');

  const ucret = gecikmeUcreti(son, son + 40);

  assert.equal(ucret, 40);
});
```

Üç bölüm boş satırlarla ayrılmıştır; ayrı bir yorum satırı gerekmez. Bu düzenin değeri
estetik değildir. Uygula bölümünün tek satır olması, testin **hangi** çağrıyı sınadığını
belirsizliğe yer bırakmadan söyler. İki çağrı varsa test düştüğünde hangisinin düşürdüğü
bilinmez. Hazırla bölümünün uzaması ise ayrı bir sinyaldir: sınanan birimin çok fazla şeye
bağlı olduğunu gösterir. Beşinci ders bu sinyali ölçer.

Testler Node çalışma zamanının yerleşik test koşucusuyla çalıştırılır. Koşucunun ayrıntılı
çıktısı süre, dosya yolu ve süreç bilgisi de içerir; bunlar ortama göre değişir. Kurs
boyunca yalnız sonuç satırları süzülür, çünkü öğretici olan bunlardır.

```sh
node --test --test-reporter=tap gecikme.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - gecikme ucreti ust siniri asmaz
# tests 1
# pass 0
# fail 1
```

Test düştü ve bu beklenen sonuçtur: hata henüz düzeltilmedi. Düşen bir koşum, testin
gerçekten bir şey sınadığının kanıtıdır. Hiç düşmemiş bir test, doğru olduğu için mi yoksa
yanlış yazıldığı için mi yeşil olduğunu söyleyemez.

#### Beklenen Değer Nereden Gelir

Doğrula bölümündeki kırk sayısı yazılı kuraldan geldi. Bu ayrıntı, testin bütün değerini
taşır. Beklenen değer gerçekleştirimin kendisinden türetildiğinde test bir **totoloji**
hâline gelir: kodun yaptığı şeyin kodun yaptığı şey olduğunu doğrular.

```js
// totoloji.test.mjs — beklenen degeri gerceklestirimle ayni yoldan hesaplayan test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GECIKME, gecikmeUcreti } from './kitaplik.mjs';

test('gecikme ucreti dogru hesaplanir', () => {
  const gecikme = 40;
  const beklenen = (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;

  assert.equal(gecikmeUcreti(1014, 1014 + gecikme), beklenen);
});
```

```sh
node --test --test-reporter=tap totoloji.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - gecikme ucreti dogru hesaplanir
# tests 1
# pass 1
# fail 0
```

Bu test, hatanın **durduğu** kod üzerinde yeşil verdi. Beklenen değeri üreten formül,
sınanan işlevin içindeki formülün aynısıdır; üst sınırı ikisi de unutur. Kalite ve Test
Temelleri kursunda tanıtılan test kâhini kavramı tam olarak bu noktayı adlandırır: beklenen
sonucun kaynağı sınanan koddan **bağımsız** olmalıdır. Kaynak yazılı kural, elle yapılmış
bir hesap, bilinen bir referans değer ya da başka bir yoldan üretilmiş bir sonuç olabilir —
ama sınanan işlevin kendi mantığı olamaz.

#### Düzeltme ve İki Testin Karşılaştırması

Düzeltme tek satırdır: ham ücret ile üst sınırın küçüğü alınır.

```js
// kitaplik.mjs — surum 2: ust sinir uygulaniyor
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export const GECIKME = { ucretsizGun: 3, gunlukUcret: 2, ustSinir: 40 };

export function sonTarih(alisGunu, uyeTuru) {
  const kural = KURALLAR[uyeTuru];
  if (kural === undefined) throw new Error(`bilinmeyen uye turu: ${uyeTuru}`);
  return alisGunu + kural.oduncGun;
}

export function gecikmeUcreti(sonGun, iadeGunu) {
  const gecikme = iadeGunu - sonGun;
  if (gecikme <= GECIKME.ucretsizGun) return 0;
  const ham = (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
  return Math.min(ham, GECIKME.ustSinir);
}
```

İki test dosyası birlikte koşturulduğunda ikisinin de durumu tersine döner.

```sh
node --test --test-reporter=tap gecikme.test.mjs totoloji.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - gecikme ucreti ust siniri asmaz
not ok 2 - gecikme ucreti dogru hesaplanir
# tests 2
# pass 1
# fail 1
```

Kurala bağlı test doğru davranışta yeşile döndü. Gerçekleştirime bağlı test ise **doğru**
kod üzerinde düştü. Bir test, kod düzeldiğinde düşüyorsa sınadığı şey davranış değildir.
Bu, sekizinci dersin konusu olan kırılganlığın en yalın biçimidir; burada yalnız
adlandırılması yeterlidir.

#### Birimin Sınırı

"Birim" sözcüğü bir boyut değil, bir **sınır** belirtir. Bir birim testi, sınadığı kodun
dışında hiçbir şeyin sonucu etkilemediği bir bağlam kurar. Uygulamada bu üç şeyi dışarıda
bırakmak demektir: disk ve ağ gibi süreç dışı kaynaklar, sistem saati ve rastgelelik
kaynakları, başka testlerin bıraktığı durum.

Bu kısıt keyfi değildir. Dışarıda bırakılan her kaynak testin süresine ve kararlılığına
bir pay ekler. Yukarıdaki testin çalışması milisaniyenin altındadır ve sonucu makineye,
saate ya da koşum sırasına bağlı değildir. Aynı testin bir veri tabanına yazması
gerekseydi, düşen bir koşumun nedeni kod, yapılandırma, ağ ya da veri kalıntısı olabilirdi
— dört olasılık arasından seçim yapmak, tek olasılığı okumaktan çok daha pahalıdır.

`sonTarih` çağrısının hazırla bölümünde durması bu sınıra bir örnektir. Sınanan birim
`gecikmeUcreti` işlevidir; `sonTarih` yalnız girdi üretir. İkisi aynı dosyada olsa bile
testin niyeti tektir ve düşen bir koşum tek bir işlevi işaret eder.

#### Özet

- Bir hata kaydının yeniden üretim adımları koşulabilir bir teste çevrilebilir; çeviriyi
  tamamlayan şey karşılaştırmanın da programa girmesidir.
- Test gövdesi hazırla, uygula ve doğrula bölümlerinden oluşur; uygula bölümünün tek çağrı
  olması düşen bir koşumun kaynağını belirsizlikten çıkarır.
- Düşen bir koşum, testin gerçekten bir şey sınadığının kanıtıdır; hiç düşmemiş bir test
  neden yeşil olduğunu söyleyemez.
- Beklenen değer sınanan koddan bağımsız bir kaynaktan gelmelidir; gerçekleştirimin
  formülünü tekrarlayan test, hatanın durduğu kodda da yeşil verir.
- Birim, bir boyut değil bir sınırdır: süreç dışı kaynaklar, saat, rastgelelik ve başka
  testlerin bıraktığı durum bu sınırın dışında kalır.

#### Sonraki Adım

Bu derste tek bir iddia kullanıldı: `assert.equal`. İki sayının eşitliği için yeterliydi,
çünkü sınanan işlev bir sayı döndürüyordu. Ödünç işlemi bir nesne, bir hata ya da bir liste
döndürdüğünde eşitlik denetimi ya çalışmaz ya da yanlış şeyi ölçer. Sonraki ders iddia
seçimini ele alır: hangi iddia hangi hatayı yakalar, aşırı geniş bir iddia ne kadar sinyal
kaybettirir ve bu kayıp nasıl ölçülür.

### İddialar

Source: https://academia.sh/tr/kurslar/birim-testi/iddialar

Eşitlik ile derin eşitliğin ayrımı, bir iddianın yakaladığı bozulma sayısıyla ölçülen sinyal gücü, kısmi eşleştirmenin gereksiz bağı kesmesi ve hata beklentisinin eşleştirmeyle daraltılması.

Önceki ders tek bir iddia kullandı: iki sayının eşitliği. Sınanan işlev bir sayı
döndürdüğü için bu yeterliydi. Ödünç verme işlemi ise bir sayı değil, bir kayıt döndürür:
üye numarası, kitap numarası, alış günü, son gün ve durum. Aynı iddia bu kayıtta bambaşka
davranır ve testin ne kadarını sınadığı, seçilen iddianın biçimine göre değişir.

Bu dersin sorusu iki katmanlıdır. Birincisi biçimseldir: hangi iddia hangi değer türü için
doğru araçtır? İkincisi ölçülebilirdir: bir iddia, sınadığı kodun kaç farklı bozulmasını
yakalar? İkinci soru birincisini kesin hâle getirir, çünkü "iyi iddia" sezgisel bir yargı
olmaktan çıkıp sayılabilir bir niceliğe dönüşür.

#### Eşitlik ve Derin Eşitlik

Sınanacak işlev, ödünç kaydını üreten `oduncVer` işlevidir. Üye türü ödünç süresini ve
açıktaki en çok ödünç sayısını belirler; sınır aşıldığında işlem reddedilir.

```js
// odunc.mjs — odunc kaydi ureten islev
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export function oduncVer(uye, kitapNo, gun) {
  const kural = KURALLAR[uye.tur];
  if (kural === undefined) throw new Error(`bilinmeyen uye turu: ${uye.tur}`);
  if (uye.aciktakiOdunc >= kural.enCokOdunc) {
    throw new Error(`odunc siniri asildi: ${kural.enCokOdunc}`);
  }
  return {
    uyeNo: uye.no,
    kitapNo,
    alisGunu: gun,
    sonGun: gun + kural.oduncGun,
    durum: 'odunc',
  };
}
```

Aynı beklentiyi iki iddia biçimiyle yazmak farkı görünür kılar.

```js
// esitlik.test.mjs — nesne donduren islevde esitlik ve derin esitlik
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncVer } from './odunc.mjs';

const UYE = { no: 'U-17', tur: 'ogrenci', aciktakiOdunc: 2 };

test('esitlik nesneyi kimlikle karsilastirir', () => {
  const kayit = oduncVer(UYE, 'K-903', 1000);

  assert.equal(kayit, {
    uyeNo: 'U-17', kitapNo: 'K-903', alisGunu: 1000, sonGun: 1028, durum: 'odunc',
  });
});

test('derin esitlik yapiyi karsilastirir', () => {
  const kayit = oduncVer(UYE, 'K-903', 1000);

  assert.deepEqual(kayit, {
    uyeNo: 'U-17', kitapNo: 'K-903', alisGunu: 1000, sonGun: 1028, durum: 'odunc',
  });
});
```

```sh
node --test --test-reporter=tap esitlik.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - esitlik nesneyi kimlikle karsilastirir
ok 2 - derin esitlik yapiyi karsilastirir
# tests 2
# pass 1
# fail 1
```

Katı eşitlik iddiası nesneleri **kimlik** üzerinden karşılaştırır: iki ayrı nesne, alanları
birebir aynı olsa da eşit sayılmaz. Derin eşitlik ise yapıyı dolaşır ve karşılık gelen
alanları karşılaştırır. Kural yalındır: sayı, dizgi ve mantıksal değer için eşitlik; nesne,
dizi ve iç içe yapı için derin eşitlik.

#### İddianın Sinyal Gücü

Bir iddianın değeri, geçtiğinde değil **düştüğünde** ortaya çıkar. Ölçüsü şudur: sınanan
kodun kaç farklı bozulmasını yakalar? Bu soru sayısal olarak yanıtlanabilir. Aşağıdaki
betik doğru kaydın beş ayrı bozulmuş sürümünü üretir ve üç iddia biçimini her birine
uygular.

```js
// iddia-gucu.mjs — uc iddia bicimi bes bozulmus gerceklestirimde denenir
import assert from 'node:assert/strict';
import { oduncVer } from './odunc.mjs';

const UYE = { no: 'U-17', tur: 'ogrenci', aciktakiOdunc: 2 };
const dogru = () => oduncVer(UYE, 'K-903', 1000);

const BOZUKLAR = {
  'sure-sabitlendi': () => ({ ...dogru(), sonGun: 1014 }),
  'durum-yanlis': () => ({ ...dogru(), durum: 'rezerve' }),
  'gun-kaydi': () => ({ ...dogru(), alisGunu: 1001 }),
  'kitap-dustu': () => { const k = dogru(); delete k.kitapNo; return k; },
  'uye-dustu': () => ({ ...dogru(), uyeNo: null }),
};

const BEKLENEN = {
  uyeNo: 'U-17', kitapNo: 'K-903', alisGunu: 1000, sonGun: 1028, durum: 'odunc',
};

const IDDIALAR = {
  'ok(kayit)': (k) => assert.ok(k),
  'equal(kayit.sonGun)': (k) => assert.equal(k.sonGun, 1028),
  'deepEqual(kayit)': (k) => assert.deepEqual(k, BEKLENEN),
};

for (const [iddiaAdi, iddia] of Object.entries(IDDIALAR)) {
  const yakalanan = [];
  for (const [bozukAd, bozuk] of Object.entries(BOZUKLAR)) {
    try {
      iddia(bozuk());
    } catch {
      yakalanan.push(bozukAd);
    }
  }
  const sayi = `${yakalanan.length}/${Object.keys(BOZUKLAR).length}`;
  console.log(`${iddiaAdi.padEnd(21)} ${sayi}  ${yakalanan.join(', ') || 'yok'}`);
}
```

```
ok(kayit)             0/5  yok
equal(kayit.sonGun)   1/5  sure-sabitlendi
deepEqual(kayit)      5/5  sure-sabitlendi, durum-yanlis, gun-kaydi, kitap-dustu, uye-dustu
```

Birinci satır aşırı geniş iddianın maliyetini gösterir. `assert.ok(kayit)` yalnız "bir
nesne döndü" der; kaydın içeriği ne olursa olsun yeşil kalır. Böyle bir test koşum
listesinde yer kaplar, süre harcar ve hiçbir bozulmayı bildirmez. Kalite ve Test Temelleri
kursunda tanıtılan yanlış alarm kavramının aynası budur: burada sinyal yanlış değil,
**yok**.

İkinci satır dar bir iddianın dürüst sınırını gösterir: tek alan sınandığında yalnız o
alanın bozulması yakalanır. Bu bir kusur değildir; testin adı da tek alanı söylüyorsa
beklenen davranış budur.

Üçüncü satır tam kapsamayı verir. Ama bu ölçüm tek yönlüdür: yakalanan bozulma sayısı
arttıkça testin **kırılganlık** riski de artar. Sonraki bölüm bu bedeli gösterir.

#### Kısmi Eşleştirme

Kayda, işlemi izlemeye yarayan bir işlem numarası eklendiği varsayılsın. Davranış
kurallarının hiçbiri değişmez; yalnız kayıtta yeni bir alan vardır.

```js
// odunc.mjs — surum 2: kayda islem numarasi eklendi
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

let sayac = 0;

export function oduncVer(uye, kitapNo, gun) {
  const kural = KURALLAR[uye.tur];
  if (kural === undefined) throw new Error(`bilinmeyen uye turu: ${uye.tur}`);
  if (uye.aciktakiOdunc >= kural.enCokOdunc) {
    throw new Error(`odunc siniri asildi: ${kural.enCokOdunc}`);
  }
  sayac += 1;
  return {
    islemNo: `I-${String(sayac).padStart(4, '0')}`,
    uyeNo: uye.no,
    kitapNo,
    alisGunu: gun,
    sonGun: gun + kural.oduncGun,
    durum: 'odunc',
  };
}
```

```sh
node --test --test-reporter=tap esitlik.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - esitlik nesneyi kimlikle karsilastirir
not ok 2 - derin esitlik yapiyi karsilastirir
# tests 2
# pass 0
# fail 2
```

Derin eşitlik iddiası düştü, çünkü tam eşleşme **tam** demektir: kayıtta beklenmedik bir
alan bulunması da uyuşmazlıktır. Oysa sınanan kural — öğrenci ödüncünün yirmi sekiz gün
sürmesi — değişmedi. Test, ilgilenmediği bir alana bağlandığı için düştü.

Çözüm iddiayı zayıflatmak değil, **daraltmaktır**: beklenen alanların varlığı ve değeri
sınanır, geri kalanı serbest bırakılır.

```js
// kismi.test.mjs — beklenen alanlarin varligini sinayan kismi eslestirme
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncVer } from './odunc.mjs';

function icerir(gercek, beklenen, yol = 'kayit') {
  for (const [alan, deger] of Object.entries(beklenen)) {
    const altYol = `${yol}.${alan}`;
    assert.ok(alan in gercek, `${altYol} alani yok`);
    if (deger !== null && typeof deger === 'object') icerir(gercek[alan], deger, altYol);
    else assert.equal(gercek[alan], deger, `${altYol} uyusmuyor`);
  }
}

const UYE = { no: 'U-17', tur: 'ogrenci', aciktakiOdunc: 2 };

test('ogrenci oduncunun son gunu yirmi sekiz gun sonradir', () => {
  const kayit = oduncVer(UYE, 'K-903', 1000);

  icerir(kayit, { uyeNo: 'U-17', kitapNo: 'K-903', sonGun: 1028, durum: 'odunc' });
});

test('kismi eslestirme yanlis alani gosterir', () => {
  const kayit = oduncVer(UYE, 'K-903', 1000);

  assert.throws(() => icerir(kayit, { sonGun: 1014 }), /kayit.sonGun uyusmuyor/);
});
```

```sh
node --test --test-reporter=tap kismi.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncunun son gunu yirmi sekiz gun sonradir
ok 2 - kismi eslestirme yanlis alani gosterir
# tests 2
# pass 2
# fail 0
```

`icerir` yardımcısı beklenen nesnenin alanlarını dolaşır, eksik alanı ve uyuşmayan değeri
ayrı iletilerle bildirir. İkinci test bu yardımcının kendisini sınar: yanlış bir beklenti
verildiğinde hangi alanın uyuşmadığını ileti içinde söylemelidir. Yardımcıyı sınamak
gereksiz görünebilir, ama bir iddia yardımcısı sessizce geçerse ona bağlı bütün testler
sinyalini yitirir.

Kısmi eşleştirmenin dozu bir seçimdir: fazla gevşek bırakılırsa `assert.ok` kadar zayıf,
fazla sıkı tutulursa tam derin eşitlik kadar bağlı olur. Ölçüt şudur: iddiada geçen her
alan, testin adında geçen kuralın parçası olmalıdır.

#### Hata Bekleme

Kural ihlali durumunda işlemin reddedilmesi de bir davranıştır ve sınanması gerekir. Hata
bekleyen iddianın eşleştirmesiz biçimi ise sık rastlanan bir yanlış alarm kaynağıdır.

```js
// hata.test.mjs — esletirmesiz ve esletirmeli hata beklentisi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncVer } from './odunc.mjs';

const SINIRDAKI = { no: 'U-17', aciktakiOdunc: 10 };

test('esletirmesiz beklenti: sinir asiminda hata verir', () => {
  assert.throws(() => oduncVer(SINIRDAKI, 'K-903', 1000));
});

test('esletirmeli beklenti: sinir asiminda hata verir', () => {
  assert.throws(() => oduncVer(SINIRDAKI, 'K-903', 1000), /odunc siniri asildi: 10/);
});
```

```sh
node --test --test-reporter=tap hata.test.mjs | grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - esletirmesiz beklenti: sinir asiminda hata verir
not ok 2 - esletirmeli beklenti: sinir asiminda hata verir
# tests 2
# pass 1
# fail 1
```

Aynı çağrı, aynı düzenek, iki farklı sonuç. Gerçekte fırlatılan hataya bakmak ayrımı
açıklar.

```js
// firlatilan.mjs — testin gercekte yakaladigi hata
import { oduncVer } from './odunc.mjs';

const SINIRDAKI = { no: 'U-17', aciktakiOdunc: 10 };
try {
  oduncVer(SINIRDAKI, 'K-903', 1000);
} catch (hata) {
  console.log(`${hata.constructor.name}: ${hata.message}`);
}
```

```
Error: bilinmeyen uye turu: undefined
```

Düzenekteki üye nesnesinde `tur` alanı unutulmuş. İşlev ödünç sınırına hiç bakmadan, daha
önceki denetimde başka bir hata fırlatıyor. Eşleştirmesiz beklenti "herhangi bir hata"
gördüğü için yeşil kaldı ve sınamak istediği kural hiç çalışmadı. Eşleştirmeli beklenti
ise düşerek düzeneğin bozuk olduğunu bildirdi.

Bu, iddia seçiminin en pahalı hatasıdır: yeşil bir test, sınadığını sandığı kuralı hiç
çalıştırmamış olabilir. Hata beklentisi her zaman bir ölçüte bağlanmalıdır — hata sınıfı,
ileti örüntüsü ya da hata nesnesinin bir alanı.

#### Özet

- Katı eşitlik nesneleri kimlikle karşılaştırır; nesne ve dizi döndüren işlevlerde derin
  eşitlik ya da kısmi eşleştirme gerekir.
- Bir iddianın sinyal gücü, sınanan kodun kaç bozulmasını yakaladığıyla ölçülür; ölçümde
  `assert.ok(kayit)` beş bozulmanın hiçbirini, derin eşitlik beşini de yakaladı.
- Tam derin eşitlik davranışla ilgisiz alanlara da bağlanır; kayda yeni bir alan eklendiğinde
  kural değişmediği hâlde test düştü.
- Kısmi eşleştirme beklenen alanları sınar, geri kalanını serbest bırakır; iddiada geçen her
  alan testin adında geçen kuralın parçası olmalıdır.
- Eşleştirmesiz hata beklentisi yanlış hatayı da kabul eder ve sınamak istediği kural hiç
  çalışmadan yeşil kalabilir.

#### Sonraki Adım

Bu derste iddiaların ne söylediği ölçüldü, ama testlerin adları sessizce arka planda kaldı.
Oysa düşen bir koşumun ilk verdiği bilgi iddia değil, **addır**: `not ok 2` satırının
yanındaki cümle. O cümle "kayit dogru" gibi bir şey söylüyorsa okuyanın kaynağa gitmesi
gerekir; kuralı söylüyorsa çoğu zaman gerekmez. Sonraki ders test adlandırmasını ele alır
ve bir test kümesinin adlarını okunabilir bir kural listesine dönüştürür.

### Test Adlandırma

Source: https://academia.sh/tr/kurslar/birim-testi/test-adlandirma

Test adının birim, koşul ve beklenen sonuç parçalarından kurulması, üç kuralı tek testte toplamanın adı yalancı kılması ve geçen testlerin adlarının okunabilir bir kural listesi oluşturması.

Düşen bir koşumun ilk verdiği bilgi iddia değil, addır. Koşucunun bildirdiği satırda önce
sonuç, sonra testin adı gelir; ayrıntılı hata iletisi bunun altındadır. Ad kuralı
söylüyorsa okuyan çoğu zaman kaynağa gitmeden ne bozulduğunu bilir. Ad "kayıt doğru" gibi
bir şey söylüyorsa, hata iletisini okumak ve oradan kaynağa gitmek zorunludur.

Bu ders adı bir biçim sorunu olmaktan çıkarıp ölçülebilir bir şeye bağlar: aynı üç kural
iki farklı biçimde yazıldığında, düşen koşumun verdiği bilgi ne kadar değişir?

#### Adın Üç Parçası

İyi bir test adı bir cümledir ve üç parça taşır: hangi **birim**, hangi **koşul** altında,
hangi **sonucu** verir. Üçü de bulunduğunda ad tek başına okunabilir bir kuraldır.

`ogrenci oduncu yirmi sekiz gun surer` cümlesinde birim ödünç süresi hesabıdır, koşul üye
türünün öğrenci olmasıdır, sonuç yirmi sekiz gündür. Buna karşılık `sonTarih testi`
adında ne koşul ne de sonuç vardır; `dogru calisir` adında ise birim de yoktur.

İki uygulama kuralı bu biçimi korur. Ad sınanan işlevin **içindeki** adımlara değil,
dışarıdan gözlenen davranışa atıf yapar: `Math.min cagrilir` bir gerçekleştirim ayrıntısı,
`ust siniri asamaz` bir davranıştır. Ad olumsuzlama yerine olumlu bir kural bildirir:
`hata vermez` yerine `sinir icindeki oduncu kabul eder`.

#### Tek Testte Üç Kural

Aşağıdaki kitaplık sürümünde üç kural birden gerilemiştir: öğrenci ödünç süresi on dört
güne düşmüş, ücretsiz gün payı sıfırlanmış ve gecikme ücretinin üst sınırı kaldırılmıştır.

```js
// kural.mjs — gerilemis surum: uc kural birden bozuk
export const KURALLAR = {
  ogrenci: { oduncGun: 14, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export const GECIKME = { ucretsizGun: 0, gunlukUcret: 2, ustSinir: 40 };

export function sonTarih(alisGunu, uyeTuru) {
  return alisGunu + KURALLAR[uyeTuru].oduncGun;
}

export function gecikmeUcreti(sonGun, iadeGunu) {
  const gecikme = iadeGunu - sonGun;
  if (gecikme <= GECIKME.ucretsizGun) return 0;
  return (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
}
```

Üç kuralı tek testte toplayan biçim, adında ancak genel bir şey söyleyebilir.

```js
// toplu.test.mjs — uc kurali tek testte dogrulayan bicim
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { sonTarih, gecikmeUcreti } from './kural.mjs';

test('odunc kurallari dogru calisir', () => {
  assert.equal(sonTarih(1000, 'ogrenci'), 1028);
  assert.equal(gecikmeUcreti(1000, 1003), 0);
  assert.equal(gecikmeUcreti(1000, 1100), 40);
});
```

```sh
node --test --test-reporter=tap toplu.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - odunc kurallari dogru calisir
# tests 1
# pass 0
# fail 1
```

Üç kural bozuk, rapor bir satır. Bunun nedeni yalnız adlandırma değildir: bir iddia
düştüğünde testin geri kalanı hiç çalışmaz. İkinci ve üçüncü iddia hiç değerlendirilmedi,
dolayısıyla o iki kuralın durumu bilinmiyor. Düzeltme yapıldıkça aynı test yeniden düşer
ve her seferinde bir sonraki kural ortaya çıkar.

Aynı üç kuralın ayrı testlere bölünmüş biçimi farklı bir rapor üretir.

```js
// ayrik.test.mjs — ayni uc kural, ucu ayri testte
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { sonTarih, gecikmeUcreti } from './kural.mjs';

test('ogrenci oduncu yirmi sekiz gun surer', () => {
  assert.equal(sonTarih(1000, 'ogrenci'), 1028);
});

test('ucretsiz gun payi icindeki iade ucretsizdir', () => {
  assert.equal(gecikmeUcreti(1000, 1003), 0);
});

test('gecikme ucreti ust siniri asmaz', () => {
  assert.equal(gecikmeUcreti(1000, 1100), 40);
});
```

```sh
node --test --test-reporter=tap ayrik.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - ogrenci oduncu yirmi sekiz gun surer
not ok 2 - ucretsiz gun payi icindeki iade ucretsizdir
not ok 3 - gecikme ucreti ust siniri asmaz
# tests 3
# pass 0
# fail 3
```

Aynı kod, aynı üç kural, aynı üç iddia. Fark ölçülebilir: birinci biçim bir kuralın
bozulduğunu, ikinci biçim üç kuralın bozulduğunu ve hangileri olduğunu bildirdi. Kaynağa
bakmadan elde edilen bilgi üçe katlandı.

Bu, ikinci dersteki sinyal gücü ölçümünün eşlik eden yarısıdır. Orada iddianın kaç
bozulmayı **yakaladığı** ölçüldü; burada yakalanan bozulmanın rapora ne kadarının
**ulaştığı** ölçülüyor. Bir testin bir davranışı doğrulaması kuralı bu iki ölçümün ortak
sonucudur, estetik bir tercih değil.

#### Adların Toplamı Bir Belirtimdir

Kurallar düzeltildiğinde geçen testlerin listesi başka bir işe yarar: kitaplığın
davranışını okunabilir biçimde anlatır.

```js
// kural.mjs — duzeltilmis surum
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export const GECIKME = { ucretsizGun: 3, gunlukUcret: 2, ustSinir: 40 };

export function sonTarih(alisGunu, uyeTuru) {
  return alisGunu + KURALLAR[uyeTuru].oduncGun;
}

export function gecikmeUcreti(sonGun, iadeGunu) {
  const gecikme = iadeGunu - sonGun;
  if (gecikme <= GECIKME.ucretsizGun) return 0;
  const ham = (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
  return Math.min(ham, GECIKME.ustSinir);
}
```

Testleri konu başlıklarına gruplamak, adın taşıması gereken yükü paylaştırır: birim ve
bağlam gruba, koşul ve sonuç teste kalır.

```js
// belirtim.test.mjs — adlari kural listesi olarak okunan test kumesi
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { sonTarih, gecikmeUcreti } from './kural.mjs';

describe('odunc suresi', () => {
  test('ogrenci uyede yirmi sekiz gundur', () => {
    assert.equal(sonTarih(1000, 'ogrenci'), 1028);
  });

  test('standart uyede on dort gundur', () => {
    assert.equal(sonTarih(1000, 'uye'), 1014);
  });
});

describe('gecikme ucreti', () => {
  test('ucretsiz gun payi icinde sifirdir', () => {
    assert.equal(gecikmeUcreti(1000, 1003), 0);
  });

  test('ucretsiz gun payi disinda gun basina ucretlendirilir', () => {
    assert.equal(gecikmeUcreti(1000, 1008), 10);
  });

  test('ust siniri asamaz', () => {
    assert.equal(gecikmeUcreti(1000, 1100), 40);
  });
});
```

```sh
node --test --test-reporter=tap belirtim.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
    ok 1 - ogrenci uyede yirmi sekiz gundur
    ok 2 - standart uyede on dort gundur
ok 1 - odunc suresi
    ok 1 - ucretsiz gun payi icinde sifirdir
    ok 2 - ucretsiz gun payi disinda gun basina ucretlendirilir
    ok 3 - ust siniri asamaz
ok 2 - gecikme ucreti
# tests 5
# pass 5
# fail 0
```

Girintili satırlar tek tek kuralları, girintisiz satırlar grupları verir. Çıktı yukarıdan
aşağıya okunduğunda ortaya bir kural listesi çıkar: ödünç süresi öğrenci üyede yirmi sekiz,
standart üyede on dört gündür; gecikme ücreti ücretsiz gün payı içinde sıfırdır, dışında
gün başına ücretlendirilir ve üst sınırı aşamaz.

Bu liste yazılı belirtimin kendisi değildir, ama onunla karşılaştırılabilir. Belirtimde
olup listede olmayan bir kural, birinci derste anılan izlenebilirlik boşluğudur. Listede
olup belirtimde olmayan bir satır ise ters yönde bir uyarıdır: ya belirtim eksik yazılmış
ya da test, kimsenin istemediği bir davranışı sabitlemiştir.

#### Ad ile İddianın Uyumu

Ad ile iddia arasında tek bir tutarlılık kuralı vardır: iddiada geçen her alan, adın
söylediği kuralın parçası olmalıdır. `ogrenci uyede yirmi sekiz gundur` adlı bir test
ayrıca kaydın durum alanını da denetliyorsa, o test iki kural sınıyor ve adı ikincisini
gizliyor demektir.

Ters yönü de aynı ölçüde bağlayıcıdır: adın söylediği kural iddiada geçmiyorsa test
adını yerine getirmiyordur. İkinci dersteki eşleştirmesiz hata beklentisi bunun bir
örneğiydi — adı ödünç sınırından söz ediyordu, gerçekte sınadığı şey ise eksik bir alandı.

Ad değişmeden iddia genişlediğinde bu uyum sessizce bozulur. Bir teste yeni bir iddia
eklenirken sorulacak soru şudur: bu iddia mevcut adın belirttiği kuralın parçası mı, yoksa yeni bir test mi
gerekiyor? İkinci yanıt çoğu zaman doğru olandır ve maliyeti üç satırdır.

#### Özet

- Test adı birim, koşul ve beklenen sonuç parçalarını taşır; üçü bulunduğunda ad tek başına
  okunabilir bir kuraldır.
- Ad, gerçekleştirim adımlarına değil dışarıdan gözlenen davranışa atıf yapar ve olumsuzlama
  yerine olumlu kural bildirir.
- Bir iddia düştüğünde testin kalan iddiaları hiç çalışmaz; üç kuralı tek teste toplayan
  biçim bir düşüş, ayrılmış biçim üç düşüş bildirdi.
- Geçen testlerin adları yukarıdan aşağıya okunduğunda bir kural listesi verir; bu liste
  yazılı belirtimle karşılaştırılabilir.
- İddiada geçen her alan adın belirttiği kuralın parçası olmalıdır; ad değişmeden iddia genişlediğinde test
  adının söylemediği bir kuralı sınamaya başlar.

#### Sonraki Adım

Buraya kadar her test yalnız kendi girdisine baktı ve her koşum aynı sonucu verdi. Testler
çoğaldıkça bu durum kendiliğinden sürmez: ortak bir düzeneği paylaşan iki test birbirinin
bıraktığı durumu görür ve koşum sırası sonucu belirlemeye başlar. Sonraki ders bu bağı
gerçek bir koşumla üretir — aynı testler farklı sırada farklı sonuç verir — ve testleri
hızlı ve birbirinden bağımsız tutan düzeneği kurar.

### Hızlı ve Bağımsız Testler

Source: https://academia.sh/tr/kurslar/birim-testi/hizli-ve-bagimsiz-testler

Modül düzeyinde paylaşılan durumun ürettiği sıra bağımlılığının sabit tohumlu permütasyonla gösterilmesi, taze düzenekle kırılması ve süreç dışı düzeneğin koşum süresine eklediği payın ölçülmesi.

Önceki dersteki testlerin hepsi tek başına doğruydu ve hepsi tek başına kaldığı sürece
doğru kalır. Testler çoğaldıkça bu koşul kendiliğinden sürmez. İki test aynı nesneyi,
aynı modül değişkenini ya da aynı dosyayı kullanıyorsa, birincinin bıraktığı durum
ikincinin girdisine karışır. Sonuç, koşum sırasının bir girdiye dönüşmesidir.

Sıra bağımlılığı sinsi bir kusurdur, çünkü çoğu zaman görünmez: koşucu testleri hep aynı
sırada çalıştırdığı sürece takım yeşil kalır. Bağımlılık, dosyaya yeni bir test eklendiğinde
ya da testler paralel dağıtıldığında ortaya çıkar ve o an bozulanın kod mu test mi olduğu
belli değildir. Bu dersin ilk işi, o sırayı bir değişkene çevirip bağımlılığı görünür
kılmaktır.

#### Sırayı Bir Değişkene Çevirmek

Sıra bağımlılığını aramanın yolu, koşum sırasını denetimli biçimde değiştirmektir. Bunun
için rastgeleliğe ihtiyaç yoktur; sabit **tohumdan** (seed) türeyen bir permütasyon yeterlidir
ve tekrarlanabilir olduğu için daha kullanışlıdır. Frontend Kalitesi kursundaki Test
Kararlılığı dersinde tanıtılan doğrusal eşlenik üreteç bu iş için yeterli bir kaynaktır.

```js
// kosucu.mjs — sabit tohumlu sira karistirmali kucuk kosucu
export function uretec(tohum) {
  let durum = (tohum * 2654435761) % 2147483647;
  return () => {
    durum = (durum * 48271) % 2147483647;
    return durum / 2147483647;
  };
}

export function karistir(dizi, tohum) {
  const rastgele = uretec(tohum);
  const kopya = [...dizi];
  for (let i = kopya.length - 1; i > 0; i -= 1) {
    const j = Math.floor(rastgele() * (i + 1));
    [kopya[i], kopya[j]] = [kopya[j], kopya[i]];
  }
  return kopya;
}

export function kos(testler, tohum) {
  const sira = karistir(Object.keys(testler), tohum);
  let gecen = 0;
  const satirlar = [];
  for (const ad of sira) {
    try {
      testler[ad]();
      gecen += 1;
      satirlar.push(`  ok      ${ad}`);
    } catch (hata) {
      satirlar.push(`  not ok  ${ad}  (${hata.message.split('\n')[0]})`);
    }
  }
  console.log(`tohum ${tohum}: ${gecen} gecti, ${sira.length - gecen} dustu`);
  console.log(satirlar.join('\n'));
}
```

Aynı tohum her zaman aynı sırayı verir; düşen bir koşum bildirildiğinde tohumu paylaşmak
sorunu yeniden üretmeye yeter.

#### Paylaşılan Durumun Ürettiği Bağımlılık

Aşağıdaki katalog modülü, ödünç kayıtlarını modül düzeyinde bir eşlemede tutar. Modül bir
kez yüklenir ve süreç boyunca tek bir eşleme vardır.

```js
// katalog.mjs — modul duzeyinde paylasilan odunc kaydi
const kayitlar = new Map();

export function oduncEkle(uyeNo, kitapNo) {
  if (kayitlar.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`);
  kayitlar.set(kitapNo, uyeNo);
}

export function iadeEt(kitapNo) {
  kayitlar.delete(kitapNo);
}

export function acikOduncSayisi() {
  return kayitlar.size;
}
```

Üç test bu modülün üç ayrı kuralını sınar. Her biri tek başına okunduğunda doğrudur.

```js
// paylasilan.mjs — paylasilan durum uzerinde tohumla belirlenen sirayla kosum
import assert from 'node:assert/strict';
import { kos } from './kosucu.mjs';
import { oduncEkle, iadeEt, acikOduncSayisi } from './katalog.mjs';

const TESTLER = {
  'yeni odunc katalogda gorunur': () => {
    oduncEkle('U-17', 'K-903');
    assert.equal(acikOduncSayisi(), 1);
  },
  'iade edilen kitap katalogdan duser': () => {
    oduncEkle('U-17', 'K-101');
    iadeEt('K-101');
    assert.equal(acikOduncSayisi(), 0);
  },
  'ayni kitap iki kez odunc verilemez': () => {
    oduncEkle('U-42', 'K-903');
    assert.throws(() => oduncEkle('U-58', 'K-903'), /zaten odunc verilmis/);
  },
};

kos(TESTLER, Number(process.argv[2] ?? 3));
```

İki farklı tohumla iki ayrı süreçte koşturmak yeter. Ayrı süreç gerekir, çünkü modül
düzeyindeki eşleme süreç ömrü boyunca yaşar.

```sh
node paylasilan.mjs 3
node paylasilan.mjs 4
```

```
tohum 3: 1 gecti, 2 dustu
  ok      yeni odunc katalogda gorunur
  not ok  iade edilen kitap katalogdan duser  (Expected values to be strictly equal:)
  not ok  ayni kitap iki kez odunc verilemez  (kitap zaten odunc verilmis: K-903)
tohum 4: 2 gecti, 1 dustu
  ok      iade edilen kitap katalogdan duser
  ok      yeni odunc katalogda gorunur
  not ok  ayni kitap iki kez odunc verilemez  (kitap zaten odunc verilmis: K-903)
```

Aynı kod, aynı üç test, iki farklı sonuç. Üçüncü tohumda geçen bir test dördüncüde düşer.
Bu tabloda hangi sonucun "doğru" olduğu sorusunun yanıtı yoktur, çünkü testler bir kuralı
değil, kendilerinden önce ne çalıştığını ölçüyor.

İki ayrı kirlenme biçimi görünüyor. `iade edilen kitap katalogdan duser` testi mutlak bir
sayı bekliyor: kataloğun boş olduğunu varsayıyor. `ayni kitap iki kez odunc verilemez`
testi ise tersine, kataloğun kendi eklediği kitabı içermediğini varsayıyor. İkisi de
başlangıç durumu hakkında yazılı olmayan bir varsayım taşıyor ve o varsayımı önceki testler
belirliyor.

#### Taze Düzenek

Çözüm, testleri sıraya duyarsız kılmak değil, her testin kendi başlangıç durumunu
kurmasıdır. Bunun için paylaşılan tek eşleme yerine, her çağrıda taze bir katalog üreten
bir fabrika kullanılır.

```js
// katalog.mjs — surum 2: her cagrida taze bir katalog uretilir
export function katalogKur() {
  const kayitlar = new Map();
  return {
    oduncEkle(uyeNo, kitapNo) {
      if (kayitlar.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`);
      kayitlar.set(kitapNo, uyeNo);
    },
    iadeEt(kitapNo) {
      kayitlar.delete(kitapNo);
    },
    acikOduncSayisi() {
      return kayitlar.size;
    },
  };
}
```

Bir testin çalışmaya başlamadan önce kurduğu bu başlangıç durumuna **düzenek** (fixture)
denir. Test koşucusu düzeneği kurmak ve gerekiyorsa sökmek için kancalar sağlar; her testten
önce çalışan kanca kuruluma, her testten sonra çalışan kanca yıkıma karşılık gelir.

```js
// bagimsiz.test.mjs — her teste taze duzenek, bildirim sirasi tohumdan gelir
import { test, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { karistir } from './kosucu.mjs';
import { katalogKur } from './katalog.mjs';

let katalog;
beforeEach(() => {
  katalog = katalogKur();
});

const TESTLER = {
  'yeni odunc katalogda gorunur': () => {
    katalog.oduncEkle('U-17', 'K-903');
    assert.equal(katalog.acikOduncSayisi(), 1);
  },
  'iade edilen kitap katalogdan duser': () => {
    katalog.oduncEkle('U-17', 'K-101');
    katalog.iadeEt('K-101');
    assert.equal(katalog.acikOduncSayisi(), 0);
  },
  'ayni kitap iki kez odunc verilemez': () => {
    katalog.oduncEkle('U-42', 'K-903');
    assert.throws(() => katalog.oduncEkle('U-58', 'K-903'), /zaten odunc verilmis/);
  },
};

for (const ad of karistir(Object.keys(TESTLER), Number(process.env.TOHUM ?? 3))) {
  test(ad, TESTLER[ad]);
}
```

Testler artık yerleşik koşucuyla çalışır; bildirim sırası tohumdan gelir, düzenek her
testten önce yeniden kurulur.

```sh
TOHUM=3 node --test --test-reporter=tap bagimsiz.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
TOHUM=4 node --test --test-reporter=tap bagimsiz.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - yeni odunc katalogda gorunur
ok 2 - iade edilen kitap katalogdan duser
ok 3 - ayni kitap iki kez odunc verilemez
# tests 3
# pass 3
# fail 0
ok 1 - iade edilen kitap katalogdan duser
ok 2 - yeni odunc katalogda gorunur
ok 3 - ayni kitap iki kez odunc verilemez
# tests 3
# pass 3
# fail 0
```

Sıra değişti, sonuç değişmedi. Bağımsızlığın ölçütü budur: koşum sırası bir girdi olmaktan
çıktığında testin bildirdiği şey yalnızca sınadığı kuraldır.

Aynı ilkenin iki uygulama sonucu vardır. Testler arasında paylaşılan hiçbir yazılabilir
durum kalmadığı için testler **paralel** koşturulabilir. Düşen bir test tek başına
koşturulduğunda da düşer; yeniden üretmek için bütün takımı çalıştırmak gerekmez.

#### Düzeneğin Süre Payı

Bağımsızlık ile hız ayrı erdemler gibi görünür, ama aynı kaynaktan beslenir. Her testin
kendi düzeneğini kurması, düzeneğin **ucuz** olmasını gerektirir. Bellekteki bir eşlemeyi
kurmak birkaç mikrosaniyedir; aynı düzeneği diskte kurmak süreç dışına çıkmayı gerektirir.

```js
// hiz.mjs — bellek ici duzenek ile disk destekli duzenegin karsilastirmasi
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const TEST_SAYISI = 200;

function bellekIciDuzenek() {
  let dosyaIslemi = 0;
  for (let i = 0; i < TEST_SAYISI; i += 1) {
    const kayitlar = new Map([['K-903', 'U-17']]);
    if (kayitlar.size !== 1) throw new Error('duzenek bozuk');
  }
  return dosyaIslemi;
}

function diskDestekliDuzenek() {
  let dosyaIslemi = 0;
  const kok = mkdtempSync(join(tmpdir(), 'katalog-'));
  for (let i = 0; i < TEST_SAYISI; i += 1) {
    const yol = join(kok, `katalog-${i}.json`);
    writeFileSync(yol, JSON.stringify({ 'K-903': 'U-17' }));
    dosyaIslemi += 1;
    const kayitlar = new Map(Object.entries(JSON.parse(readFileSync(yol, 'utf8'))));
    dosyaIslemi += 1;
    if (kayitlar.size !== 1) throw new Error('duzenek bozuk');
  }
  rmSync(kok, { recursive: true, force: true });
  return dosyaIslemi;
}

function olc(islev) {
  const baslangic = performance.now();
  const dosyaIslemi = islev();
  return { sure: performance.now() - baslangic, dosyaIslemi };
}

const bellek = olc(bellekIciDuzenek);
const disk = olc(diskDestekliDuzenek);

console.log(`bellek ici duzenek   : ${TEST_SAYISI} test, ${bellek.dosyaIslemi} dosya islemi`);
console.log(`disk destekli duzenek: ${TEST_SAYISI} test, ${disk.dosyaIslemi} dosya islemi`);
console.log(`disk destekli surum daha uzun surdu : ${disk.sure > bellek.sure}`);
console.log(`sure farki en az yirmi kat           : ${disk.sure / bellek.sure >= 20}`);
```

```
bellek ici duzenek   : 200 test, 0 dosya islemi
disk destekli duzenek: 200 test, 400 dosya islemi
disk destekli surum daha uzun surdu : true
sure farki en az yirmi kat           : true
```

Mutlak süreler makineye ve dosya sistemine bağlıdır; bu yüzden çıktı ham milisaniye yerine
karşılaştırma sonucu bildiriyor. Değişmeyen şey ilişkinin yönüdür: iki yüz testlik bir
takım, test başına iki dosya işlemiyle dört yüz süreç dışı çağrı üretir ve bu çağrıların
her biri sabit bir taban maliyet taşır.

Buradan çıkan kural bir hız hedefi değil, bir sınır tanımıdır. Birinci derste "birim"
sınırının süreç dışı kaynakları dışarıda bıraktığı söylenmişti; bu ölçüm o kısıtın bedelini
gösteriyor. Diskle, ağla ya da veri tabanıyla çalışan testler yazılmayacak demek değildir —
o testlerin adı **tümleşim testidir** ve ayrı bir katmanda, daha seyrek koşturulur.

#### Özet

- Paylaşılan yazılabilir durum, koşum sırasını testin girdilerinden birine dönüştürür.
- Sabit tohumdan türeyen bir permütasyon sıra bağımlılığını tekrarlanabilir biçimde ortaya
  çıkarır; aynı üç test iki tohumda iki farklı sonuç verdi.
- Kirlenme iki yönde işler: bir test kataloğun boş, başka bir test kataloğun dolu olmadığını
  varsaydı; ikisinin de varsayımını önceki testler belirledi.
- Her testin kendi düzeneğini kurması sırayı sonuçtan bağımsız kılar ve testleri paralel
  koşturulabilir yapar.
- Bağımsızlık ucuz düzenek gerektirir: iki yüz testlik bir takımda süreç dışı düzenek dört
  yüz dosya işlemi ekledi ve koşumu belirgin biçimde uzattı.

#### Sonraki Adım

Bu derste düzenek testin elindeydi: katalog nesnesi bir fabrika çağrısıyla üretiliyordu.
Sınanan kod bağımlılığını kendi içinde yarattığında bu olanak kalmaz — testin
değiştiremeyeceği bir saat, bir rastgelelik kaynağı ya da bir depo, testin girdisi olmaktan
çıkar. Sonraki ders aynı iş kuralını iki biçimde yazar, bağımlılığın içeriden yaratıldığı
sürümle dışarıdan verildiği sürümü karşılaştırır ve testin kurduğu düzeneği satır sayısıyla
ölçer.

### Test Edilebilir Tasarım

Source: https://academia.sh/tr/kurslar/birim-testi/test-edilebilir-tasarim

Bağımlılığını kendi içinde yaratan sürüm ile bağımlılığı dışarıdan alan sürümün karşılaştırılması, düzenek maliyetinin satırla ölçülmesi ve zaman ile rastgeleliğin dışarıdan verilmesi.

Önceki derste düzenek testin elindeydi: katalog nesnesi bir fabrika çağrısıyla üretiliyor,
her test kendi kopyasını kuruyordu. Bu olanak koddan geldi, testten değil. Sınanan kod
kataloğu kendi içinde yaratsaydı, testin elinde onu değiştirecek bir kol kalmazdı.

Test edilebilirlik testin bir niteliği değil, **tasarımın** bir niteliğidir. Aynı iş kuralı
iki farklı biçimde yazıldığında ikisi de doğru çalışır, ama biri sınanabilir bir kol
bırakır, diğeri bırakmaz. Bu dersin sorusu şudur: bu fark nerede ortaya çıkar ve ne kadar
maliyet üretir?

#### Aynı Kural, İki Tasarım

Sınanacak iş şudur: ödünç verme işlemi bugünün gün numarasını okur, bir fiş numarası
üretir, kitabın halihazırda ödünçte olmadığını denetler ve ödünç kaydını döndürür. İşlemin
üç dış kaynağı vardır: saat, fiş numarası üreteci ve katalog.

Birinci sürüm üçünü de kendi içinde yaratır.

```js
// iceriden.mjs — bagimliliklarini kendi iceride yaratan surum
const katalog = new Map();

export function katalogTemizle() {
  katalog.clear();
}

export function oduncVer(uyeNo, kitapNo, uyeTuru) {
  const bugun = Math.floor(Date.now() / 86400000);
  const fisNo = `F-${String(Math.floor(Math.random() * 1000000)).padStart(6, '0')}`;
  if (katalog.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`);
  const sure = uyeTuru === 'ogrenci' ? 28 : 14;
  katalog.set(kitapNo, uyeNo);
  return { fisNo, uyeNo, kitapNo, alisGunu: bugun, sonGun: bugun + sure };
}
```

İkinci sürüm üçünü de dışarıdan alır. İş kuralı — süre hesabı, çift ödünç denetimi, kayıt
biçimi — birebir aynıdır.

```js
// disaridan.mjs — bagimliliklari disaridan verilen surum
export function oduncServisiKur({ saat, fisUretici, katalog }) {
  return {
    oduncVer(uyeNo, kitapNo, uyeTuru) {
      const bugun = saat();
      const fisNo = fisUretici();
      if (katalog.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`);
      const sure = uyeTuru === 'ogrenci' ? 28 : 14;
      katalog.set(kitapNo, uyeNo);
      return { fisNo, uyeNo, kitapNo, alisGunu: bugun, sonGun: bugun + sure };
    },
  };
}
```

Bu ikinci biçime **bağımlılık enjeksiyonu** (dependency injection) denir; bağımlılık
içeride yaratılmak yerine dışarıdan verilir. Adı
kulağa bir çatı özelliği gibi gelir, ama burada görüldüğü gibi yalnız bir imza kararıdır:
kod bağımlılığını aramak yerine kendisine verilmesini bekler.

#### Düzeneğin Maliyeti

İki sürümün testleri aynı kuralı sınar; farkı, o kurala ulaşmak için kurulması gereken
düzenek yaratır.

```js
// duzenek.mjs — iki surumun testinin kurdugu duzenek
import { katalogTemizle, oduncVer } from './iceriden.mjs';
import { oduncServisiKur } from './disaridan.mjs';

export function iceridenDuzenek() {
  const gercekNow = Date.now;
  const gercekRandom = Math.random;
  Date.now = () => 1000 * 86400000;
  Math.random = () => 0.123456;
  katalogTemizle();
  const geriAl = () => {
    Date.now = gercekNow;
    Math.random = gercekRandom;
  };
  return { servis: { oduncVer }, geriAl };
}

export function disaridanDuzenek() {
  const servis = oduncServisiKur({
    saat: () => 1000,
    fisUretici: () => 'F-123456',
    katalog: new Map(),
  });
  return { servis, geriAl: () => {} };
}
```

Düzeneklerin boyutu ölçülebilir. İki düzenek de çalıştırılıp ürettikleri kayıt
karşılaştırıldığında, farkın davranışta değil yalnız hazırlıkta olduğu görülür.

```js
// olcum.mjs — iki duzenegin satir sayisi ve urettikleri kayit
import { iceridenDuzenek, disaridanDuzenek } from './duzenek.mjs';

const satir = (islev) => islev.toString().split('\n').length - 2;

for (const [ad, duzenek] of [['iceriden', iceridenDuzenek], ['disaridan', disaridanDuzenek]]) {
  const { servis, geriAl } = duzenek();
  const kayit = servis.oduncVer('U-17', 'K-903', 'ogrenci');
  geriAl();
  console.log(`${ad.padEnd(10)} duzenek ${String(satir(duzenek)).padStart(2)} satir  ${JSON.stringify(kayit)}`);
}
```

```
iceriden   duzenek 10 satir  {"fisNo":"F-123456","uyeNo":"U-17","kitapNo":"K-903","alisGunu":1000,"sonGun":1028}
disaridan  duzenek  6 satir  {"fisNo":"F-123456","uyeNo":"U-17","kitapNo":"K-903","alisGunu":1000,"sonGun":1028}
```

İki kayıt birebir aynı. Düzenek maliyeti on satıra karşı altı satır. Ama asıl fark satır
sayısı değil, o satırların **ne yaptığıdır**. Dışarıdan verilen sürümün düzeneği yalnız
nesne kurar. İçeriden yaratan sürümün düzeneği ise iki küresel işlevi değiştirir, sınanan
modülün iç durumunu temizler ve değiştirdiklerini geri almayı üstlenir.

Birinci derste "hazırla bölümünün uzaması ayrı bir sinyaldir" denmişti. Ölçülen sinyal
budur: düzenek büyüdükçe testin sınadığı kural azalmaz, ama teste giren risk artar.

#### Geri Alınmayan Değişikliğin Bedeli

Küresel bir işlevi değiştiren düzeneğin taşıdığı risk somuttur. Aşağıdaki test dosyasında
birinci test saati dondurur ve geri almayı unutur.

```js
// sizinti.test.mjs — geri alinmayan kuresel degisiklik sonraki testi etkiler
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { katalogTemizle, oduncVer } from './iceriden.mjs';

test('ogrenci oduncu yirmi sekiz gun surer', () => {
  Date.now = () => 1000 * 86400000;
  katalogTemizle();

  const kayit = oduncVer('U-17', 'K-903', 'ogrenci');

  assert.equal(kayit.sonGun, 1028);
});

test('gun numarasi gercek saatten gelir', () => {
  const bugun = Math.floor(Date.now() / 86400000);

  assert.ok(bugun > 20000, `beklenmedik gun numarasi: ${bugun}`);
});
```

```sh
node --test --test-reporter=tap sizinti.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncu yirmi sekiz gun surer
not ok 2 - gun numarasi gercek saatten gelir
# tests 2
# pass 1
# fail 1
```

İkinci test kendi başına doğrudur ve tek başına koşturulduğunda geçer. Düştüğü koşumda
gördüğü gün numarası bindir; çünkü saat hâlâ birinci testin bıraktığı gibi donmuştur.
Bu, önceki dersteki sıra bağımlılığının aynısıdır, ama kaynağı farklıdır: paylaşılan durum
kitaplığın kendisinde değil, **çalışma zamanının küresel nesnelerinde** birikiyor.

Küresel değişikliği geri almak bir çözümdür, ama unutulabilir bir çözümdür ve unutulduğunda
bozulan test, hatayı yapan test değildir. Dışarıdan verilen sürümde bu risk sınıfı hiç
oluşmaz.

```js
// temiz.test.mjs — disaridan verilen bagimlilik kuresel durumu bozmaz
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncServisiKur } from './disaridan.mjs';

const kur = () => oduncServisiKur({
  saat: () => 1000,
  fisUretici: () => 'F-123456',
  katalog: new Map(),
});

test('ogrenci oduncu yirmi sekiz gun surer', () => {
  const servis = kur();

  const kayit = servis.oduncVer('U-17', 'K-903', 'ogrenci');

  assert.equal(kayit.sonGun, 1028);
});

test('gun numarasi gercek saatten gelir', () => {
  const bugun = Math.floor(Date.now() / 86400000);

  assert.ok(bugun > 20000, `beklenmedik gun numarasi: ${bugun}`);
});

test('fis numarasi dis kaynaktan gelir', () => {
  const servis = kur();

  const kayit = servis.oduncVer('U-17', 'K-903', 'uye');

  assert.equal(kayit.fisNo, 'F-123456');
});
```

```sh
node --test --test-reporter=tap temiz.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncu yirmi sekiz gun surer
ok 2 - gun numarasi gercek saatten gelir
ok 3 - fis numarasi dis kaynaktan gelir
# tests 3
# pass 3
# fail 0
```

Üçüncü test, ilk iki sürümde hiç yazılamayan bir şeyi sınıyor: fiş numarasının nereden
geldiği. İçeriden yaratan sürümde fiş numarası rastgelelik kaynağından gelir ve testin
onu doğrulaması için yine küresel bir değişiklik gerekir. Dışarıdan verilen sürümde
üreteç bir parametredir ve testin verdiği değer aynen kayda geçer.

#### Belirlenimci Olmayan Kaynakların Yeri

Zaman ve rastgelelik, testin denetleyemediği kaynakların iki tipik örneğidir; aynı sınıfa
süreç kimliği, ortam değişkenleri, ağ adresleri ve dosya sistemi de girer. Ortak yanları,
**aynı girdiye aynı çıktıyı vermemeleridir**.

Bunları sınanabilir kılmanın yolu, kodun bu kaynaklara doğrudan erişmesini engellemek değil,
erişimi tek bir noktada toplayıp o noktayı dışarıdan verilebilir hâle getirmektir. Yukarıdaki
sürümde `saat` ve `fisUretici` bu noktalardır. Üretimde çalışan yapılandırma gerçek kaynakları
verir; testte çalışan yapılandırma sabit değerler verir.

Bu ayrımın bir sınırı vardır ve baştan söylenmelidir: bir kod parçası zamanı gerçekten
okumak zorundaysa, o okuma da bir yerde sınanmalıdır. Dışarıdan verme, o sınamayı ortadan
kaldırmaz — birim testinin dışına, tümleşim testine taşır. Kazanç, iş kuralının belirlenimci
bir bağlamda sınanabilmesidir.

#### Özet

- Test edilebilirlik testin değil tasarımın niteliğidir; aynı iş kuralı iki biçimde de doğru
  çalışır, ama yalnız biri sınanabilir bir kol bırakır.
- Bağımlılığın dışarıdan verilmesi bir çatı özelliği değil bir imza kararıdır: kod
  bağımlılığını aramak yerine kendisine verilmesini bekler.
- Ölçümde iki düzenek aynı kaydı üretti; içeriden yaratan sürümün düzeneği on, dışarıdan
  alan sürümünki altı satırdı ve fark yalnız uzunlukta değil, küresel değişiklik içermesindeydi.
- Geri alınmayan küresel değişiklik sonraki testi düşürür; bozulan test, hatayı yapan test
  değildir.
- Zaman, rastgelelik ve benzeri belirlenimci olmayan kaynaklar tek bir noktada toplanıp
  dışarıdan verilebilir kılınır; bu, o kaynakların sınanmasını kaldırmaz, tümleşim testine
  taşır.

#### Sonraki Adım

Bu derste dışarıdan verilen bağımlılıklar sabit değer döndüren küçük işlevlerdi: `saat`
her zaman bin, `fisUretici` her zaman aynı numarayı verdi. Bu, dört türü olan bir ailenin
yalnız bir üyesidir. Bir bağımlılık yalnız yer doldurmak için de verilebilir, çağrıldığında
kayıt tutmak için de, gerçek davranışın çalışan ama basitleştirilmiş bir kopyası olmak için
de. Sonraki ders bu türleri tek tek elle yazar ve her birinin hangi soruyu yanıtladığını
ayırır.

### Sahte Bağımlılıklar

Source: https://academia.sh/tr/kurslar/birim-testi/sahte-bagimliliklar

Kukla nesne, güdük, taklit nesne, casus nesne ve sahte nesne türlerinin elle yazılması, her türün yanıtladığı sorunun ayrılması ve aynı değişiklikte hangi türün konuştuğunun ölçülmesi.

Önceki derste dışarıdan verilen bağımlılıklar sabit bir değer döndüren küçük işlevlerdi.
Bu, bir ailenin tek üyesidir. Sınanan koda gerçek bağımlılık yerine konan nesnelerin ortak
adı **sahte bağımlılıktır** (test double) ve aralarında yapı değil **amaç** farkı vardır:
bir tanesi yalnız parametre yerini doldurur, bir tanesi belirli bir yanıt verir, bir tanesi
gerçek davranışın çalışan bir kopyasıdır, bir tanesi çağrıları kaydeder, bir tanesi de
beklentiyi kendi içinde taşır.

Bu ayrımlar akademik değildir. Her tür farklı bir doğrulama sorusunu yanıtlar ve yanlış tür
seçildiğinde test ya hiçbir şey sınamaz ya da sınamak istemediği bir şeye bağlanır. Bu ders
beş türü de elle yazar ve sonunda hepsini aynı değişiklik karşısında sınar.

#### Sınanan Servis

Ödünç servisi dört bağımlılık alır: kayıtları tutan katalog, üyeye haber veren bildirim
kanalı, işlem günlüğü ve gün numarasını veren saat.

```js
// servis.mjs — odunc servisi: katalog, bildirim, gunluk ve saat bagimliliklariyla
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export function oduncServisiKur({ katalog, bildirim, gunluk, saat }) {
  return {
    oduncVer(uye, kitapNo) {
      const kural = KURALLAR[uye.tur];
      if (katalog.uyeOduncSayisi(uye.no) >= kural.enCokOdunc) {
        throw new Error(`odunc siniri asildi: ${kural.enCokOdunc}`);
      }
      const bugun = saat();
      const kayit = { uyeNo: uye.no, kitapNo, alisGunu: bugun, sonGun: bugun + kural.oduncGun };
      katalog.ekle(kayit);
      bildirim.gonder(uye.no, `son tarih: ${kayit.sonGun}`);
      return kayit;
    },

    iadeAl(kitapNo) {
      const kayit = katalog.bul(kitapNo);
      if (kayit === undefined) throw new Error(`odunc kaydi yok: ${kitapNo}`);
      katalog.sil(kitapNo);
      gunluk.yaz(`iade ${kitapNo}`);
      return kayit;
    },
  };
}
```

#### Beş Türün Yazımı

Beşi de birkaç satırdır; hiçbiri bir kütüphane gerektirmez.

```js
// sahteler.mjs — dort sahte bagimlilik turu ve beklentili bir sahte nesne
export function bosNesne(ad) {
  return new Proxy({}, {
    get: (_hedef, alan) => () => {
      throw new Error(`${ad}.${String(alan)} cagrilmamaliydi`);
    },
  });
}

export function gudukKatalog(oduncSayisi) {
  return {
    uyeOduncSayisi: () => oduncSayisi,
    ekle() {},
    bul: () => undefined,
    sil() {},
  };
}

export function taklitKatalog() {
  const kayitlar = new Map();
  return {
    uyeOduncSayisi: (uyeNo) => [...kayitlar.values()].filter((k) => k.uyeNo === uyeNo).length,
    ekle: (kayit) => kayitlar.set(kayit.kitapNo, kayit),
    bul: (kitapNo) => kayitlar.get(kitapNo),
    sil: (kitapNo) => kayitlar.delete(kitapNo),
  };
}

export function casusNesne(...yontemler) {
  const cagrilar = [];
  const nesne = { cagrilar };
  for (const ad of yontemler) nesne[ad] = (...girdi) => cagrilar.push([ad, ...girdi]);
  return nesne;
}

export function sahteNesne(beklenen, ...yontemler) {
  const casus = casusNesne(...yontemler);
  return {
    ...casus,
    dogrula() {
      const gercek = JSON.stringify(casus.cagrilar);
      const bek = JSON.stringify(beklenen);
      if (gercek === bek) return;
      throw new Error(`beklenti karsilanmadi: beklenen ${bek}, gercek ${gercek}`);
    },
  };
}
```

**Kukla nesne** (dummy) yalnız parametre yerini doldurur; çağrılmaması beklenir. Buradaki
sürüm bu beklentiyi zorunlu kılar: herhangi bir yöntemi çağrıldığında hata fırlatır.

**Güdük** (stub) sınanan kodun okuduğu bir değeri sabitler. Sorusu şudur: kod bu girdiyle
hangi yolu izler? Güdüğün kendisi hiçbir şey doğrulamaz.

**Taklit nesne** (fake) gerçek bağımlılığın çalışan ama basitleştirilmiş bir kopyasıdır.
Buradaki katalog gerçek bir eşleme kullanır; ekleme, arama ve silme anlamlıdır. Çok adımlı
senaryolar ancak bu türle kurulabilir.

**Casus nesne** (spy) çağrıları kaydeder ve testin sonradan incelemesine bırakır. Sorusu
şudur: bu çağrı gerçekten yapıldı mı, hangi bağımsız değişkenlerle?

**Sahte nesne** (mock) beklentiyi kendi içinde taşır. Casus nesneden farkı, doğrulamanın
testin değil nesnenin sorumluluğunda olmasıdır.

#### Her Türün Yanıtladığı Soru

Beş tür, beş ayrı testte kullanıldığında farkları görünür olur.

```js
// sahteler.test.mjs — her sahte bagimlilik turu ayri bir soruyu yanitlar
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncServisiKur } from './servis.mjs';
import { bosNesne, gudukKatalog, taklitKatalog, casusNesne, sahteNesne } from './sahteler.mjs';

const OGRENCI = { no: 'U-17', tur: 'ogrenci' };
const UYE = { no: 'U-42', tur: 'uye' };

test('bos nesne: odunc verme gunluge hic dokunmaz', () => {
  const servis = oduncServisiKur({
    katalog: taklitKatalog(),
    bildirim: casusNesne('gonder'),
    gunluk: bosNesne('gunluk'),
    saat: () => 1000,
  });

  const kayit = servis.oduncVer(OGRENCI, 'K-903');

  assert.equal(kayit.sonGun, 1028);
});

test('guduk: siniri dolu uyede odunc reddedilir', () => {
  const servis = oduncServisiKur({
    katalog: gudukKatalog(5),
    bildirim: bosNesne('bildirim'),
    gunluk: bosNesne('gunluk'),
    saat: () => 1000,
  });

  assert.throws(() => servis.oduncVer(UYE, 'K-903'), /odunc siniri asildi: 5/);
});

test('taklit nesne: iade edilen kitap yeniden odunc verilebilir', () => {
  const servis = oduncServisiKur({
    katalog: taklitKatalog(),
    bildirim: casusNesne('gonder'),
    gunluk: casusNesne('yaz'),
    saat: () => 1000,
  });

  servis.oduncVer(OGRENCI, 'K-903');
  servis.iadeAl('K-903');
  const ikinci = servis.oduncVer(UYE, 'K-903');

  assert.equal(ikinci.sonGun, 1014);
});

test('casus nesne: uyeye tek bildirim gider', () => {
  const casus = casusNesne('gonder');
  const servis = oduncServisiKur({
    katalog: taklitKatalog(),
    bildirim: casus,
    gunluk: bosNesne('gunluk'),
    saat: () => 1000,
  });

  servis.oduncVer(OGRENCI, 'K-903');

  assert.deepEqual(casus.cagrilar, [['gonder', 'U-17', 'son tarih: 1028']]);
});

test('sahte nesne: reddedilen oduncte bildirim gitmez', () => {
  const sahte = sahteNesne([], 'gonder');
  const servis = oduncServisiKur({
    katalog: gudukKatalog(5),
    bildirim: sahte,
    gunluk: bosNesne('gunluk'),
    saat: () => 1000,
  });

  assert.throws(() => servis.oduncVer(UYE, 'K-903'));

  sahte.dogrula();
});
```

```sh
node --test --test-reporter=tap sahteler.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - bos nesne: odunc verme gunluge hic dokunmaz
ok 2 - guduk: siniri dolu uyede odunc reddedilir
ok 3 - taklit nesne: iade edilen kitap yeniden odunc verilebilir
ok 4 - casus nesne: uyeye tek bildirim gider
ok 5 - sahte nesne: reddedilen oduncte bildirim gitmez
# tests 5
# pass 5
# fail 0
```

Beş testin doğrula bölümleri birbirinden farklıdır ve fark tür seçiminden gelir. Birinci
testin iddiası dönen kayıtla ilgilidir; kukla nesne yalnız bir varsayımı korur. İkinci test
yine dönüş değerine bakar; güdük ise o dönüşü mümkün kılan girdiyi verir. Üçüncü test üç
adımlık bir senaryo kurar ve bu ancak durum tutan bir taklit nesneyle olur. Dördüncü ve
beşinci testler dönüş değerine değil, **yapılan çağrıya** bakar.

Son iki testin ayrımı incedir. Casus nesne kullanan test doğrulamayı kendi gövdesinde
yapar ve iddia açıkça okunur. Sahte nesne kullanan test beklentiyi düzenekte kurar ve
gövdesinde yalnız `dogrula()` çağırır. İkincisi, birden çok çağrı beklentisi olan
durumlarda gövdeyi kısaltır; karşılığında beklentinin nerede kurulduğu testin başına taşınır.

#### Aynı Değişiklikte Kim Konuşur

Türlerin gerçek farkı, kod değiştiğinde hangisinin ses çıkardığıdır. Aşağıdaki betik
servise iki yeni yan etki ekler — her ödünç denemesi günlüğe yazılır ve reddedilen istekte
de üyeye bildirim gider — ve aynı üç türü bu yeni sürüme uygular.

```js
// sinama.mjs — servis yan etki kazandiginda hangi sahte bagimlilik konusur
import assert from 'node:assert/strict';
import { KURALLAR } from './servis.mjs';
import { bosNesne, gudukKatalog, sahteNesne } from './sahteler.mjs';

function degismisServisKur({ katalog, bildirim, gunluk, saat }) {
  return {
    oduncVer(uye, kitapNo) {
      const kural = KURALLAR[uye.tur];
      gunluk.yaz(`odunc denemesi ${kitapNo}`);
      if (katalog.uyeOduncSayisi(uye.no) >= kural.enCokOdunc) {
        bildirim.gonder(uye.no, 'odunc reddedildi');
        throw new Error(`odunc siniri asildi: ${kural.enCokOdunc}`);
      }
      const bugun = saat();
      const kayit = { uyeNo: uye.no, kitapNo, alisGunu: bugun, sonGun: bugun + kural.oduncGun };
      katalog.ekle(kayit);
      bildirim.gonder(uye.no, `son tarih: ${kayit.sonGun}`);
      return kayit;
    },
  };
}

const UYE = { no: 'U-42', tur: 'uye' };
const sessizGunluk = { yaz() {} };
const sessizBildirim = { gonder() {} };

function dene(ad, govde) {
  try {
    govde();
    console.log(`${ad.padEnd(12)} sessiz`);
  } catch (hata) {
    console.log(`${ad.padEnd(12)} ${hata.message.split('\n')[0]}`);
  }
}

dene('guduk', () => {
  const servis = degismisServisKur({
    katalog: gudukKatalog(5),
    bildirim: sessizBildirim,
    gunluk: sessizGunluk,
    saat: () => 1000,
  });

  assert.throws(() => servis.oduncVer(UYE, 'K-903'), /odunc siniri asildi: 5/);
});

dene('bos nesne', () => {
  const servis = degismisServisKur({
    katalog: gudukKatalog(0),
    bildirim: sessizBildirim,
    gunluk: bosNesne('gunluk'),
    saat: () => 1000,
  });

  servis.oduncVer(UYE, 'K-903');
});

dene('sahte nesne', () => {
  const sahte = sahteNesne([], 'gonder');
  const servis = degismisServisKur({
    katalog: gudukKatalog(5),
    bildirim: sahte,
    gunluk: sessizGunluk,
    saat: () => 1000,
  });

  assert.throws(() => servis.oduncVer(UYE, 'K-903'));
  sahte.dogrula();
});
```

```
guduk        sessiz
bos nesne    gunluk.yaz cagrilmamaliydi
sahte nesne  beklenti karsilanmadi: beklenen [], gercek [["gonder","U-42","odunc reddedildi"]]
```

Üç satır, tür seçiminin ne anlama geldiğini tek başına anlatır.

Güdük **sessiz** kaldı, çünkü dönüş değeri ve fırlatılan hata değişmemişti. Bu bir kusur
değildir: güdük yalnız girdi sağlar, doğrulama yapmaz. Dönüş değerine bakan bir test yeni
yan etkileri görmez ve görmemelidir.

Kukla nesne **konuştu**, çünkü çağrılmaması gereken bir bağımlılık çağrıldı. Bu türün tek
işlevi budur ve karşılığında testin dayandığı varsayımı belge hâline getirir.

Sahte nesne de **konuştu**, çünkü beklentisi "hiç bildirim gitmesin" idi ve bir bildirim
gitti. Aynı sinyali casus nesne de verirdi; fark, doğrulamanın nerede durduğudur.

Buradan çıkan seçim kuralı üç maddedir. Doğrulanacak şey **dönüş değeri ya da fırlatılan
hata** ise güdük yeter. **Yan etkinin gerçekleşmesi** doğrulanacaksa casus ya da sahte
nesne gerekir. **Çok adımlı bir senaryo** kurulacaksa taklit nesne gerekir; güdükle
kurulan çok adımlı senaryolar hızla tutarsız hâle gelir.

Çağrı doğrulaması bir maliyet taşır: test, sınanan kodun **nasıl** çalıştığına bağlanır.
Bir yeniden düzenleme bildirim gönderimini başka bir katmana taşırsa casus ve sahte
nesneyle yazılmış testler düşer, oysa dışarıdan gözlenen davranış aynıdır. Sekizinci ders
bu bedeli ölçer.

#### Koşucunun Sağladığı Yetenek

Yerleşik test koşucusu, çağrı kaydı tutan işlevleri elle yazmak zorunda kalmamak için bir
yetenek sağlar. Elle yazılan casus nesnenin yaptığı işi aynı yapar; kayıt biçimi hazır gelir.

```js
// yerlesik.test.mjs — kosucunun sagladigi sahte islev yetenegi
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { oduncServisiKur } from './servis.mjs';
import { bosNesne, taklitKatalog } from './sahteler.mjs';

test('yerlesik sahte islev cagri kaydini tutar', () => {
  const gonder = mock.fn();
  const servis = oduncServisiKur({
    katalog: taklitKatalog(),
    bildirim: { gonder },
    gunluk: bosNesne('gunluk'),
    saat: () => 1000,
  });

  servis.oduncVer({ no: 'U-17', tur: 'ogrenci' }, 'K-903');

  assert.equal(gonder.mock.callCount(), 1);
  assert.deepEqual(gonder.mock.calls[0].arguments, ['U-17', 'son tarih: 1028']);
});
```

```sh
node --test --test-reporter=tap yerlesik.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - yerlesik sahte islev cagri kaydini tutar
# tests 1
# pass 1
# fail 0
```

Hazır yetenek yazım yükünü azaltır, ama tür seçimini yapmaz. `mock.fn()` çağrı kaydı
tuttuğu için casus nesne olarak da sahte nesne olarak da kullanılabilir; hangisi olduğunu
belirleyen şey testin onu nasıl doğruladığıdır. Türü seçen karar hâlâ yazan kişinindir.

#### Özet

- Sahte bağımlılıkların farkı yapıda değil amaçtadır; beş tür beş ayrı doğrulama sorusunu
  yanıtlar.
- Kukla nesne yalnız yer doldurur, güdük girdi sabitler, taklit nesne çalışan bir kopyadır,
  casus nesne çağrıları kaydeder, sahte nesne beklentiyi kendi taşır.
- Servise iki yan etki eklendiğinde güdük sessiz kaldı, kukla nesne beklenmedik çağrıyı, sahte
  nesne karşılanmayan beklentiyi bildirdi.
- Dönüş değeri sınanacaksa güdük yeter; yan etki sınanacaksa casus ya da sahte nesne, çok
  adımlı senaryo kurulacaksa taklit nesne gerekir.
- Çağrı doğrulaması testi kodun nasıl çalıştığına bağlar; koşucunun sağladığı hazır yetenek
  yazım yükünü azaltır ama tür seçimini yapmaz.

#### Sonraki Adım

Bu derste testlerin neyi sınadığı tür tür ayrıldı, ama takımın **ne kadarını** sınadığı
sorusu hiç sorulmadı. Bu soruya verilen yaygın yanıt bir yüzdedir: kodun kaçta kaçı test
sırasında çalıştı? Sonraki ders bu yüzdenin üç ayrı biçimde hesaplandığını gösterir ve
üçünü de elle sayar. Yüzde yüz satır kapsamına ulaşmış bir takımın kaçırdığı hata, aynı
derste koşturularak gösterilir.

### Kapsam Ölçümü

Source: https://academia.sh/tr/kurslar/birim-testi/kapsam-olcumu

Satır, dal ve yol kapsamının aynı işlev üzerinde elle sayılması, yüzde yüz satır kapsamına ulaşmış bir takımın kaçırdığı hatanın koşturularak gösterilmesi ve kapsam yüzdesinin bir hedef olarak kullanılmasının sınırı.

Önceki dersler testlerin **neyi** sınadığını ayırdı: hangi iddia hangi bozulmayı yakalar,
hangi sahte bağımlılık hangi soruyu yanıtlar. Sorulmayan soru şudur: yazılan testler
kitaplığın ne kadarına dokundu?

Bu soruya verilen yaygın yanıt bir yüzdedir ve o yüzde en az üç farklı biçimde hesaplanır.
Üç ölçüm aynı kod üzerinde farklı sayılar verir; aralarındaki fark, ölçümün ne söylediğini
belirler. Bu ders üçünü aynı işlev üzerinde elle sayar.

#### Ölçülecek İşlev

Gecikme ücreti hesabı iki koşul içerir: ücretsiz gün payının aşılması ve kitabın ödünç
alındığı şubeden başka bir şubeye iade edilmesi.

```js
// ucret.mjs — gecikme ve sube disi iade ucreti
export const GECIKME = { ucretsizGun: 3, gunlukUcret: 2, ustSinir: 40, subeDisiUcret: 5 };

export function gecikmeUcreti(kayit, iade) {
  const gecikme = iade.gun - kayit.sonGun;
  let ucret = 0;
  if (gecikme > GECIKME.ucretsizGun) {
    ucret = (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
  }
  if (iade.sube !== kayit.sube) {
    ucret += GECIKME.subeDisiUcret;
  }
  return Math.min(ucret, GECIKME.ustSinir);
}
```

İki test yazılmış. İkisi de geçiyor.

```js
// kapsam.test.mjs — iki test, yuzde yuz satir kapsami
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gecikmeUcreti } from './ucret.mjs';

const KAYIT = { sonGun: 1014, sube: 'merkez' };

test('gecikmis iade gun basina ucretlendirilir', () => {
  assert.equal(gecikmeUcreti(KAYIT, { gun: 1024, sube: 'merkez' }), 14);
});

test('gecikmis sube disi iadeye ek ucret eklenir', () => {
  assert.equal(gecikmeUcreti(KAYIT, { gun: 1024, sube: 'yamac' }), 19);
});
```

```sh
node --test --test-reporter=tap kapsam.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - gecikmis iade gun basina ucretlendirilir
ok 2 - gecikmis sube disi iadeye ek ucret eklenir
# tests 2
# pass 2
# fail 0
```

#### Üç Ölçümün Elle Sayımı

Kapsam ölçümü, sayılacak birim seçildiğinde tanımlanır.

**Satır kapsamı** (statement coverage) çalıştırılan satırların toplam satıra oranıdır.
**Dal kapsamı** (branch coverage) her koşulun hem doğru hem yanlış sonucunun görülüp
görülmediğini sayar. **Yol kapsamı** (path coverage) koşul sonuçlarının **birleşimlerini**
sayar: iki koşullu bir işlevde dört, üç koşulluda sekiz yol vardır.

Aynı işlevin iz tutan bir kopyası bu üç sayıyı doğrudan verir.

```js
// olcum.mjs — ayni islevin satir, dal ve yol izini tutan surumu
import { GECIKME } from './ucret.mjs';

const satirlar = new Set();
const dallar = new Set();
const yollar = new Set();

function izlenenUcret(kayit, iade) {
  satirlar.add('S1');
  const gecikme = iade.gun - kayit.sonGun;
  satirlar.add('S2');
  let ucret = 0;
  satirlar.add('S3');
  const d1 = gecikme > GECIKME.ucretsizGun;
  dallar.add(d1 ? 'D1-dogru' : 'D1-yanlis');
  if (d1) {
    satirlar.add('S4');
    ucret = (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
  }
  satirlar.add('S5');
  const d2 = iade.sube !== kayit.sube;
  dallar.add(d2 ? 'D2-dogru' : 'D2-yanlis');
  if (d2) {
    satirlar.add('S6');
    ucret += GECIKME.subeDisiUcret;
  }
  satirlar.add('S7');
  yollar.add(`${d1 ? 'D' : 'Y'}${d2 ? 'D' : 'Y'}`);
  return Math.min(ucret, GECIKME.ustSinir);
}

const KAYIT = { sonGun: 1014, sube: 'merkez' };
izlenenUcret(KAYIT, { gun: 1024, sube: 'merkez' });
izlenenUcret(KAYIT, { gun: 1024, sube: 'yamac' });

const TUM_SATIR = ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7'];
const TUM_DAL = ['D1-dogru', 'D1-yanlis', 'D2-dogru', 'D2-yanlis'];
const TUM_YOL = ['DD', 'DY', 'YD', 'YY'];

const rapor = (ad, tumu, gorulen) => {
  const eksik = tumu.filter((x) => !gorulen.has(x));
  const oran = Math.round(((tumu.length - eksik.length) / tumu.length) * 100);
  console.log(`${ad} ${tumu.length - eksik.length}/${tumu.length} (%${oran})  eksik: ${eksik.join(', ') || 'yok'}`);
};

rapor('satir kapsami', TUM_SATIR, satirlar);
rapor('dal kapsami  ', TUM_DAL, dallar);
rapor('yol kapsami  ', TUM_YOL, yollar);
```

```
satir kapsami 7/7 (%100)  eksik: yok
dal kapsami   3/4 (%75)  eksik: D1-yanlis
yol kapsami   2/4 (%50)  eksik: YD, YY
```

Aynı iki test, aynı işlev, üç farklı sayı. Satır ölçümü tam görünüyor: yedi satırın yedisi
de çalıştı. Dal ölçümü bir eksik bildiriyor: birinci koşulun yanlış sonucu — yani gecikmesiz
bir iade — hiç denenmedi. Yol ölçümü iki eksik bildiriyor: gecikmesiz iadenin iki şube
durumu da hiç oluşmadı.

Üç sayının sırası rastlantı değildir. Satır kapsamı her zaman dal kapsamından büyük ya da
ona eşittir; dal kapsamı da yol kapsamından. Bir satırın çalışması için bir dalın
girilmesi yeter, ama bir dalın her iki sonucunun görülmesi için ayrı girdiler gerekir ve
yolların birleşimi için daha da fazlası.

#### Yüzde Yüz Satır Kapsamının Kaçırdığı

Kapsanmayan iki yol boş bir olasılık değil, çalıştırılabilir birer girdidir.

```js
// kacan.mjs — kapsanmayan yollarda islevin verdigi sonuclar
import { gecikmeUcreti } from './ucret.mjs';

const KAYIT = { sonGun: 1014, sube: 'merkez' };
const DURUMLAR = [
  ['YD  gecikmesiz, sube disi', { gun: 1016, sube: 'yamac' }, 0],
  ['YY  gecikmesiz, ayni sube', { gun: 1016, sube: 'merkez' }, 0],
];

for (const [ad, iade, beklenen] of DURUMLAR) {
  const gercek = gecikmeUcreti(KAYIT, iade);
  console.log(`${ad}: ucret ${gercek}, beklenen ${beklenen} -> ${gercek === beklenen ? 'uyumlu' : 'HATALI'}`);
}
```

```
YD  gecikmesiz, sube disi: ucret 5, beklenen 0 -> HATALI
YY  gecikmesiz, ayni sube: ucret 0, beklenen 0 -> uyumlu
```

Yazılı kural şubeler arası iade ücretini gecikme ücretinin bir eki olarak tanımlar:
zamanında iade edilen bir kitap, hangi şubeye bırakılırsa bırakılsın ücretsizdir.
Gerçekleştirim ise şube denetimini gecikmeden bağımsız uyguluyor ve zamanında iade eden
üyeden beş birim alıyor.

Bu hata, satır kapsamı yüzde yüz olan bir takımın altında duruyordu. Ölçüm yanlış değildi;
**sorduğu soru** dardı. Satır ölçümü "bu satır çalıştı mı" diye sorar, "bu satır hangi
koşullar altında çalıştı" diye sormaz.

Eksik yollar teste çevrildiğinde hata rapora düşer.

```js
// kapsam.test.mjs — surum 2: dort yolun dordu de sinaniyor
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gecikmeUcreti } from './ucret.mjs';

const KAYIT = { sonGun: 1014, sube: 'merkez' };

test('gecikmis iade gun basina ucretlendirilir', () => {
  assert.equal(gecikmeUcreti(KAYIT, { gun: 1024, sube: 'merkez' }), 14);
});

test('gecikmis sube disi iadeye ek ucret eklenir', () => {
  assert.equal(gecikmeUcreti(KAYIT, { gun: 1024, sube: 'yamac' }), 19);
});

test('ucretsiz gun payi icindeki iade ucretsizdir', () => {
  assert.equal(gecikmeUcreti(KAYIT, { gun: 1016, sube: 'merkez' }), 0);
});

test('ucretsiz gun payi icindeki sube disi iade de ucretsizdir', () => {
  assert.equal(gecikmeUcreti(KAYIT, { gun: 1016, sube: 'yamac' }), 0);
});
```

```sh
node --test --test-reporter=tap kapsam.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - gecikmis iade gun basina ucretlendirilir
ok 2 - gecikmis sube disi iadeye ek ucret eklenir
ok 3 - ucretsiz gun payi icindeki iade ucretsizdir
not ok 4 - ucretsiz gun payi icindeki sube disi iade de ucretsizdir
# tests 4
# pass 3
# fail 1
```

Düzeltme, ücretsiz gün payı içindeki iadeyi erken dönüşle ayırır.

```js
// ucret.mjs — surum 2: sube disi ucreti yalniz gecikmis iadede alinir
export const GECIKME = { ucretsizGun: 3, gunlukUcret: 2, ustSinir: 40, subeDisiUcret: 5 };

export function gecikmeUcreti(kayit, iade) {
  const gecikme = iade.gun - kayit.sonGun;
  if (gecikme <= GECIKME.ucretsizGun) return 0;
  let ucret = (gecikme - GECIKME.ucretsizGun) * GECIKME.gunlukUcret;
  if (iade.sube !== kayit.sube) {
    ucret += GECIKME.subeDisiUcret;
  }
  return Math.min(ucret, GECIKME.ustSinir);
}
```

```sh
node --test --test-reporter=tap kapsam.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - gecikmis iade gun basina ucretlendirilir
ok 2 - gecikmis sube disi iadeye ek ucret eklenir
ok 3 - ucretsiz gun payi icindeki iade ucretsizdir
ok 4 - ucretsiz gun payi icindeki sube disi iade de ucretsizdir
# tests 4
# pass 4
# fail 0
```

Erken dönüşün ikinci bir etkisi vardır: ücretsiz gün payı içindeki iadede şube koşulu artık
hiç değerlendirilmez. Dört yol üçe iner, çünkü iki yol tek bir çıkışta birleşir. Yol
sayısını azaltmak, kapsamı artırmanın test yazmaktan başka bir yoludur ve genellikle daha
ucuzdur.

#### Yüzdenin Okunması

Kapsam yüzdesinin doğru okunuşu tek yönlüdür. Düşük bir kapsam **kesin** bilgi verir:
kodun bir bölümü hiç çalıştırılmadı. Yüksek bir kapsam ise hiçbir şeyin kanıtı değildir;
kodun çalıştırıldığını söyler, doğru davrandığını değil. İkinci derste ölçülen totoloji
testi yüzde yüz kapsam üretir ve tek bir bozulmayı yakalamaz.

Bundan iki uygulama sonucu çıkar. Kapsam bir **hedef** olarak kullanıldığında ölçüt bozulur:
sayıyı yükseltmenin en ucuz yolu iddiasız test yazmaktır ve bu, sinyal üretmeden yüzdeyi
yükseltir. Kapsam bir **liste** olarak kullanıldığında ise doğrudan işe yarar: kapsanmayan
dal ve yollar, henüz sorulmamış soruların adıdır. Yukarıdaki hata tam olarak o listeden
çıktı.

Yerleşik test koşucusunun kapsam raporu üreten bir seçeneği vardır. Bu seçeneğin adı,
raporun biçimi ve deneysel olup olmadığı çalışma zamanı sürümüne bağlıdır; ürettiği tablo
mutlak dosya yollarını ve çalıştırılan ortamın ayrıntılarını içerir. Bu yüzden burada
kapsam elle sayıldı: ölçümün **tanımı** araçtan bağımsızdır, çıktısı değildir.

#### Özet

- Kapsam ölçümü sayılacak birim seçildiğinde tanımlanır; satır, dal ve yol kapsamı aynı kod
  üzerinde farklı sayılar verir.
- Ölçümde iki test satır kapsamını yüzde yüze, dal kapsamını yüzde yetmiş beşe, yol kapsamını
  yüzde elliye çıkardı.
- Satır kapsamı her zaman dal kapsamından, dal kapsamı da yol kapsamından büyük ya da ona
  eşittir.
- Kapsanmayan yollardan biri gerçek bir hata barındırıyordu: zamanında iade edilen kitaba
  şube dışı ücreti uygulanıyordu.
- Düşük kapsam kesin bilgi verir, yüksek kapsam hiçbir şeyin kanıtı değildir; kapsam bir
  hedef olarak değil, sorulmamış soruların listesi olarak okunur.

#### Sonraki Adım

Bu derste testler kodun içine bakmadan yazıldı: girdi verildi, dönüş değeri denetlendi.
Altıncı dersteki casus ve sahte nesneler ise kodun içine baktı — hangi çağrının yapıldığını
sınadılar. İki yaklaşım aynı davranışı doğrulasa da yeniden düzenleme karşısında aynı
davranmaz. Sonraki ders bir yeniden düzenleme gerçekten yapar ve iki test biçiminden
hangisinin kırıldığını koşumla gösterir.

### Kırılgan Testler

Source: https://academia.sh/tr/kurslar/birim-testi/kirilgan-testler

Gerçekleştirime bağlanan testlerin davranış değişmeden yeniden düzenlemede kırılması, aynı yeniden düzenlemede davranışa bağlı testlerin yeşil kalması ve kırılganlığın düşme oranıyla ölçülmesi.

Önceki derste testler kodun içine bakmadan yazıldı: girdi verildi, dönüş değeri denetlendi.
Altıncı derste ise casus ve sahte nesneler kodun içine baktı — hangi çağrının yapıldığını
sınadılar. İki yaklaşım aynı davranışı doğrulayabilir, ama aynı değişiklik karşısında aynı
davranmaz.

Bir testin **kırılgan** (flaky) olması, sınadığı davranış değişmediği hâlde düşmesi
demektir. Kırılganlığın iki kaynağı vardır: testin belirlenimci olmayan bir şeye bağlanması
ve testin gerçekleştirime bağlanması. Dördüncü ve beşinci dersler birincisini gösterdi.
Bu ders ikincisini bir yeniden düzenlemeyle ölçer.

#### Sınanan Birim

Rezervasyon sırası, ödünçteki bir kitap için bekleyen üyeleri tutar. Sözleşmesi dört
maddedir: üye sıraya eklenir ve bekleme konumunu öğrenir, kitap sıradaki ilk üyeye ayrılır,
aynı üye iki kez sıraya giremez ve her durum değişiminde üyeye bildirim gider. Ayrıca her
işlem bir günlüğe yazılır.

```js
// rezervasyon.mjs — surum 1: dizi tabanli rezervasyon sirasi
export function rezervasyonKur({ bildirim, gunluk }) {
  const sira = [];
  return {
    sira,

    ekle(uyeNo) {
      if (sira.includes(uyeNo)) throw new Error(`uye zaten sirada: ${uyeNo}`);
      sira.push(uyeNo);
      gunluk.yaz(`ekle ${uyeNo} konum ${sira.length}`);
      bildirim.gonder(uyeNo, `sira: ${sira.length}`);
      return sira.length;
    },

    sonraki() {
      const uyeNo = sira.shift();
      if (uyeNo === undefined) return undefined;
      gunluk.yaz(`sonraki ${uyeNo}`);
      bildirim.gonder(uyeNo, 'kitap ayrildi');
      return uyeNo;
    },

    uzunluk: () => sira.length,
  };
}
```

```js
// casus.mjs — cagri kaydi tutan kucuk sahte bagimlilik
export function casusNesne(...yontemler) {
  const cagrilar = [];
  const nesne = { cagrilar };
  for (const ad of yontemler) nesne[ad] = (...girdi) => cagrilar.push([ad, ...girdi]);
  return nesne;
}
```

#### İki Test Kümesi

Birinci küme yalnız dışarıdan gözlenebilen şeyleri sınar: dönüş değerleri, fırlatılan hata
ve üyeye giden bildirim. Üyeye giden bildirim bir yan etkidir ve casus nesneyle
doğrulanır — ama sözleşmenin parçası olduğu için davranış sayılır.

```js
// davranis.test.mjs — yalniz disaridan gozlenen davranisi sinar
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { rezervasyonKur } from './rezervasyon.mjs';
import { casusNesne } from './casus.mjs';

const kur = () => {
  const bildirim = casusNesne('gonder');
  return { kuyruk: rezervasyonKur({ bildirim, gunluk: casusNesne('yaz') }), bildirim };
};

test('ekleme sirasi bekleme konumunu belirler', () => {
  const { kuyruk } = kur();

  assert.equal(kuyruk.ekle('U-17'), 1);
  assert.equal(kuyruk.ekle('U-42'), 2);
});

test('kitap siradaki ilk uyeye ayrilir', () => {
  const { kuyruk } = kur();
  kuyruk.ekle('U-17');
  kuyruk.ekle('U-42');

  assert.equal(kuyruk.sonraki(), 'U-17');
  assert.equal(kuyruk.sonraki(), 'U-42');
  assert.equal(kuyruk.sonraki(), undefined);
});

test('ayni uye siraya iki kez giremez', () => {
  const { kuyruk } = kur();
  kuyruk.ekle('U-17');

  assert.throws(() => kuyruk.ekle('U-17'), /uye zaten sirada: U-17/);
  assert.equal(kuyruk.uzunluk(), 1);
});

test('uyeye konumunu bildiren ileti gider', () => {
  const { kuyruk, bildirim } = kur();

  kuyruk.ekle('U-17');
  kuyruk.sonraki();

  assert.deepEqual(bildirim.cagrilar, [
    ['gonder', 'U-17', 'sira: 1'],
    ['gonder', 'U-17', 'kitap ayrildi'],
  ]);
});
```

İkinci küme aynı birimi sınar, ama iç yapıya ve günlük metnine bağlanır. İkisi de
sözleşmenin dışındadır: sırayı diziyle mi eşlemeyle mi tuttuğu, günlüğe hangi cümleyi
yazdığı üyeyi ilgilendirmez.

```js
// gerceklestirim.test.mjs — ic yapiya ve gunluk metnine baglanan testler
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { rezervasyonKur } from './rezervasyon.mjs';
import { casusNesne } from './casus.mjs';

const kur = () => {
  const gunluk = casusNesne('yaz');
  return { kuyruk: rezervasyonKur({ bildirim: casusNesne('gonder'), gunluk }), gunluk };
};

test('sira ic dizisi ekleme sirasini tutar', () => {
  const { kuyruk } = kur();

  kuyruk.ekle('U-17');
  kuyruk.ekle('U-42');

  assert.deepEqual(kuyruk.sira, ['U-17', 'U-42']);
});

test('gunluge yazilan satirlar belirli bicimdedir', () => {
  const { kuyruk, gunluk } = kur();

  kuyruk.ekle('U-17');
  kuyruk.sonraki();

  assert.deepEqual(gunluk.cagrilar, [
    ['yaz', 'ekle U-17 konum 1'],
    ['yaz', 'sonraki U-17'],
  ]);
});
```

```sh
node --test --test-reporter=tap davranis.test.mjs gerceklestirim.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ekleme sirasi bekleme konumunu belirler
ok 2 - kitap siradaki ilk uyeye ayrilir
ok 3 - ayni uye siraya iki kez giremez
ok 4 - uyeye konumunu bildiren ileti gider
ok 5 - sira ic dizisi ekleme sirasini tutar
ok 6 - gunluge yazilan satirlar belirli bicimdedir
# tests 6
# pass 6
# fail 0
```

Altı testin altısı da yeşil. Bu noktada iki kümenin farkı görünmez; ikisi de aynı kodu
doğruluyor gibidir.

#### Yeniden Düzenleme

Sıra bir eşlemeye taşınır, iç dizi dışarıya açılmaz ve günlük satırları tek bir kalıba
oturtulur. Sözleşmenin dört maddesi de aynen korunur: dönüş değerleri, fırlatılan hata ve
üyeye giden iletiler değişmez.

```js
// rezervasyon.mjs — surum 2: sira eslemeye tasindi, gunluk bicimi tek kaliba baglandi
export function rezervasyonKur({ bildirim, gunluk }) {
  const kayitlar = new Map();
  let sonSiraNo = 0;
  const siradakiler = () => [...kayitlar.entries()]
    .sort((a, b) => a[1] - b[1])
    .map(([uyeNo]) => uyeNo);

  return {
    ekle(uyeNo) {
      if (kayitlar.has(uyeNo)) throw new Error(`uye zaten sirada: ${uyeNo}`);
      sonSiraNo += 1;
      kayitlar.set(uyeNo, sonSiraNo);
      const konum = kayitlar.size;
      gunluk.yaz(`rezervasyon uye=${uyeNo} durum=eklendi konum=${konum}`);
      bildirim.gonder(uyeNo, `sira: ${konum}`);
      return konum;
    },

    sonraki() {
      const [ilk] = siradakiler();
      if (ilk === undefined) return undefined;
      kayitlar.delete(ilk);
      gunluk.yaz(`rezervasyon uye=${ilk} durum=ayrildi`);
      bildirim.gonder(ilk, 'kitap ayrildi');
      return ilk;
    },

    uzunluk: () => kayitlar.size,
  };
}
```

Aynı iki dosya, aynı komut, yeniden düzenlenmiş kitaplık.

```sh
node --test --test-reporter=tap davranis.test.mjs gerceklestirim.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ekleme sirasi bekleme konumunu belirler
ok 2 - kitap siradaki ilk uyeye ayrilir
ok 3 - ayni uye siraya iki kez giremez
ok 4 - uyeye konumunu bildiren ileti gider
not ok 5 - sira ic dizisi ekleme sirasini tutar
not ok 6 - gunluge yazilan satirlar belirli bicimdedir
# tests 6
# pass 4
# fail 2
```

Ölçüm nettir: dört davranış testi yeşil kaldı, iki gerçekleştirim testi düştü. Kitaplığın
sözleşmesi değişmedi — hiçbir üye farklı bir sonuç görmez. Düşen iki test bir gerileme
bildirmedi; **kendi varsayımlarının** bozulduğunu bildirdi.

Bu, kırılganlığın maliyetidir. Yeniden düzenleme, davranışı korurken iç yapıyı iyileştirmek
demektir; bunu her yaptığında düşen bir test kümesi, yeniden düzenlemeyi pahalı kılar.
Yeterince pahalı olduğunda yapılmaz olur ve kod, kendisini koruyan testler yüzünden
iyileştirilemez hâle gelir.

Buradan çıkan sınır sorusu şudur: bir iddia, sözleşmenin parçası olan bir şeye mi bağlı?
Dördüncü davranış testi de bir casus nesne kullanıyor ve o da bir yan etkiyi sınıyor — ama
üyeye giden bildirim sözleşmededir. Ayrımı yapan şey aracın türü değil, doğrulanan şeyin
kimin gözlediği bir şey olduğudur.

#### Kırılganlığın Ölçüsü

Kırılganlığın diğer kaynağı belirlenimci olmayan girdilerdir ve orada ölçü **düşme oranıdır**
(flake rate): aynı testin aynı kodda yapılan çok sayıda koşumunda düşme yüzdesi.

```js
// dusme-orani.mjs — kirilgan bir testin dusme orani sabit tohumla olculur
function uretec(tohum) {
  let durum = (tohum * 2654435761) % 2147483647;
  return () => {
    durum = (durum * 48271) % 2147483647;
    return durum / 2147483647;
  };
}

// Toplu isin suresi makineye ve yuke gore degisir: kirk ile altmis dokuz arasi.
const sureUret = (rastgele) => 40 + Math.floor(rastgele() * 30);

function butceyeBagliTest(rastgele) {
  const sure = sureUret(rastgele);
  if (sure > 68) throw new Error(`sure butcesi asildi: ${sure}`);
}

function davranisaBagliTest(rastgele) {
  const sure = sureUret(rastgele);
  if (Number.isInteger(sure) === false || sure < 0) throw new Error(`gecersiz sure: ${sure}`);
}

const KOSUM = 1000;
for (const [ad, test] of [['sure butcesine bagli', butceyeBagliTest], ['davranisa bagli', davranisaBagliTest]]) {
  const rastgele = uretec(20);
  let dusen = 0;
  for (let i = 0; i < KOSUM; i += 1) {
    try {
      test(rastgele);
    } catch {
      dusen += 1;
    }
  }
  const oran = ((dusen / KOSUM) * 100).toFixed(1);
  console.log(`${ad.padEnd(21)} ${KOSUM} kosumda ${dusen} dusus, dusme orani %${oran}`);
}
```

```
sure butcesine bagli  1000 kosumda 27 dusus, dusme orani %2.7
davranisa bagli       1000 kosumda 0 dusus, dusme orani %0.0
```

Yüzde iki virgül yedilik bir oran, tam da fark edilmesi güç olduğu için tehlikelidir.
Elli koşumun kırk dokuzunda yeşil olan bir test "arada bir düşüyor" diye anılır ve düştüğü
koşum yeniden çalıştırılır. O anda takımın anlamı bozulur: yeşil bir koşum artık "kod doğru"
demez, "bu sefer şanslıydık" demeye başlar.

Düşme oranı ölçüldüğünde yapılacak iş bellidir. Kırılgan test **karantinaya** alınır — ana
koşumdan çıkarılır ki kalan testlerin sinyali temiz kalsın — ve kaynağı düzeltilir.
Yukarıdaki örnekte düzeltme, süre bütçesini bir birim testine yükletmemektir; süre bir
başarım ölçütüdür ve ayrı ölçülür.

#### Özet

- Kırılgan test, sınadığı davranış değişmediği hâlde düşen testtir; iki kaynağı belirlenimci
  olmayan girdiler ve gerçekleştirime bağlanmadır.
- Sıra dizisi eşlemeye taşındığında ve günlük biçimi değiştiğinde dört davranış testi yeşil
  kaldı, iki gerçekleştirim testi düştü; kitaplığın sözleşmesi hiç değişmemişti.
- Gerçekleştirime bağlı testler yeniden düzenlemeyi pahalı kılar; yeterince pahalı olduğunda
  kod, kendisini koruyan testler yüzünden iyileştirilemez hâle gelir.
- Sınırı çizen soru araç değil sınırdır: doğrulanan şey sözleşmenin parçası mı, yoksa iç
  ayrıntı mı?
- Belirlenimci olmayan kırılganlığın ölçüsü düşme oranıdır; ölçümde süre bütçesine bağlanan
  test bin koşumun yirmi yedisinde düştü, davranışa bağlanan test hiç düşmedi.

#### Sonraki Adım

Birim Test Pratiği konusu burada kapanıyor. Buraya kadar her testte kod önce vardı: bir
hata raporu, bir kural ya da bir yeniden düzenleme, sınanacak bir gerçekleştirimi hazır
buldu. Sonraki konu bu sırayı tersine çevirir. Test önce yazıldığında hangi kodun
yazılacağını test belirler; kırmızı bir koşumla başlayan döngü, yeşile ulaştıktan sonra
yeniden düzenlemeyle biter. Sonraki ders bu üç adımı tek tek koşturur.

## Test Odaklı Geliştirme

Kırmızı–yeşil–yeniden düzenle döngüsünün adımları, adım boyunun geri bildirim süresine etkisi, örneklerle sözleşme tanımı, kabul ölçütünün otomatikleşmesi ve dikiş açarak eski kodun test altına alınması.

### Kırmızı–Yeşil–Yeniden Düzenle

Source: https://academia.sh/tr/kurslar/birim-testi/kirmizi-yesil-yeniden-duzenle

Testin koddan önce yazıldığı üç adımlı döngünün her adımının koşturularak gösterilmesi, sabit değerle geçilen yeşilin üçgenlemeyle genelleştirilmesi ve yeniden düzenleme adımının güvence koşulu.

Birim Test Pratiği konusunda her testte kod önce vardı: bir hata raporu, bir kural ya da bir
yeniden düzenleme, sınanacak bir gerçekleştirimi hazır buldu. Test o gerçekleştirimi
doğruladı, ölçtü ya da kırılganlığını gösterdi.

Bu konu sırayı tersine çevirir. Test önce yazıldığında artık bir doğrulama aracı değil, bir
**tasarım aracıdır**: hangi işlevin hangi imzayla var olacağını, hangi girdiyi alacağını ve
ne döndüreceğini test belirler. Bu tersine çevirmenin adı **test odaklı geliştirmedir** (test
driven development) ve üç adımlı bir döngüyle işler.

Kurulacak yeni kural ödünç yenilemesidir. Bir üye elindeki kitabın süresini uzatabilir;
yenilenen ödüncün son tarihi, yenileme gününden itibaren üye türünün ödünç süresi kadar
ileri alınır.

#### Kırmızı: Henüz Olmayan Kodun Testi

Döngü, geçmeyen bir testle başlar. Testin çalışabilmesi için sınanan işlevin **var olması**
gerekir; bu yüzden ilk yazılan şey, yalnız derlenmeye yetecek kadar boş bir gerçekleştirimdir.

```js
// yenileme.mjs — surum 0: islev var, davranis yok
export function yenile(kayit, bugun) {
  return kayit;
}
```

Testin kendisi, henüz yazılmamış davranışı tarif eder.

```js
// yenileme.test.mjs — kirmizi adim: tek kural, henuz gerceklestirim yok
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { yenile } from './yenileme.mjs';

test('ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir', () => {
  const kayit = { uyeNo: 'U-17', uyeTuru: 'ogrenci', sonGun: 1028 };

  const yeni = yenile(kayit, 1020);

  assert.equal(yeni.sonGun, 1048);
});
```

```sh
node --test --test-reporter=tap yenileme.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir
# tests 1
# pass 0
# fail 1
```

Bu adımda üç karar aynı anda verildi ve hiçbiri gerçekleştirim yazılırken verilmedi.
İşlevin adı `yenile`, girdileri ödünç kaydı ile gün numarası, çıktısı güncellenmiş bir
kayıt. Test odaklı geliştirmenin tasarım tarafı budur: arayüz, onu **kullanan** koddan
türer.

Kırmızı koşumun güvencesi birinci derste anılan şeydir: testin düşebildiği görüldü.
Yazıldığı anda yeşil olan bir test, yanlış bir şeyi sınıyor ya da hiçbir şeyi sınamıyor
olabilir; bu ihtimal ancak düşen bir koşum görülerek elenir.

#### Yeşil: En Kısa Yol

İkinci adımın tek ölçütü testi geçirmektir. Doğru tasarım, genel çözüm ya da temiz kod bu
adımın hedefi değildir; onlar üçüncü adımın işidir.

```js
// yenileme.mjs — surum 1: testi gecirmenin en kisa yolu
export function yenile(kayit, bugun) {
  return { ...kayit, sonGun: 1048 };
}
```

```sh
node --test --test-reporter=tap yenileme.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir
# tests 1
# pass 1
# fail 0
```

Sabit değer döndüren bu sürüm açıkça yanlıştır ve kasıtlıdır. Yaptığı iş, testin gerçekten
o değeri sınadığını kanıtlamaktır: kırmızıdan yeşile geçişin nedeni yazılan satırdır,
başka bir şey değil.

Bu adım aynı zamanda bir teşhis aracıdır. Sabit değer yazıldığı hâlde test yeşile dönmüyorsa
sorun gerçekleştirimde değil, testin kendisindedir.

#### Üçgenleme

Sabit değerin yanlışlığını kod değil, **ikinci bir örnek** ortaya çıkarır. Aynı kuralın
farklı bir girdiyle yazılmış hâli, gerçekleştirimi genelleşmeye zorlar. Bu tekniğe
**üçgenleme** (triangulation) denir.

```js
// yenileme.test.mjs — surum 2: ikinci uye turu eklendi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { yenile } from './yenileme.mjs';

test('ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir', () => {
  const kayit = { uyeNo: 'U-17', uyeTuru: 'ogrenci', sonGun: 1028 };

  const yeni = yenile(kayit, 1020);

  assert.equal(yeni.sonGun, 1048);
});

test('standart uye oduncu yenilendiginde son tarih bugunden on dort gun sonradir', () => {
  const kayit = { uyeNo: 'U-42', uyeTuru: 'uye', sonGun: 1014 };

  const yeni = yenile(kayit, 1020);

  assert.equal(yeni.sonGun, 1034);
});
```

```sh
node --test --test-reporter=tap yenileme.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir
not ok 2 - standart uye oduncu yenilendiginde son tarih bugunden on dort gun sonradir
# tests 2
# pass 1
# fail 1
```

Döngü yeniden kırmızıya döndü ve bu sefer kırmızının işaret ettiği şey belirgin: sabit
değer yetmiyor. Genelleştirme artık bir tahmin değil, düşen testin dayattığı bir zorunluluk.

```js
// yenileme.mjs — surum 2: sabit deger yerine kural tablosu
export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } };

export function yenile(kayit, bugun) {
  return { ...kayit, sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun };
}
```

```sh
node --test --test-reporter=tap yenileme.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir
ok 2 - standart uye oduncu yenilendiginde son tarih bugunden on dort gun sonradir
# tests 2
# pass 2
# fail 0
```

#### Yeniden Düzenle: Yeşilin Üzerinde Çalışmak

Üçüncü adımın tanımı kesindir: **yeniden düzenleme** (refactoring), dışarıdan gözlenen
davranışı değiştirmeden iç yapıyı iyileştirmektir. Yeni kural eklenmez, yeni durum
ele alınmaz, hata düzeltilmez. Yalnız yapı değişir.

```js
// yenileme.mjs — surum 3: sure hesabi adlandirilmis bir isleve alindi
export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } };

export function oduncSuresi(uyeTuru) {
  return KURALLAR[uyeTuru].oduncGun;
}

export function yenile(kayit, bugun) {
  return { ...kayit, sonGun: bugun + oduncSuresi(kayit.uyeTuru) };
}
```

```sh
node --test --test-reporter=tap yenileme.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci oduncu yenilendiginde son tarih bugunden yirmi sekiz gun sonradir
ok 2 - standart uye oduncu yenilendiginde son tarih bugunden on dort gun sonradir
# tests 2
# pass 2
# fail 0
```

Bu adımı mümkün kılan şey, ondan önceki iki adımdır. Yeşil bir test kümesi olmadan yeniden
düzenleme bir yeniden yazımdır ve doğruluğu ancak gözle denetlenebilir. Testler yeşilken
yapılan her yapı değişikliği, koşumun kendisiyle doğrulanır.

Sekizinci dersteki ölçüm burada karşılığını bulur. Testler gerçekleştirime bağlıysa bu adım
kırmızıya döner ve yeniden düzenleme cezalandırılır. Testler davranışa bağlıysa üçüncü adım
serbesttir. Test odaklı geliştirme, testleri kullanan koddan türettiği için davranışa bağlı
test yazmayı kolaylaştırır — ama garanti etmez; sözleşme ile iç ayrıntı ayrımını yine yazan
kişi yapar.

#### Üç Adımın Böldüğü Sorular

Döngünün değeri, adımlarının aynı anda düşünülmesi gereken üç soruyu birbirinden ayırmasıdır.

Kırmızı adımda tek soru şudur: **ne** istiyorum? Kural nedir, arayüz nasıl kullanılacak,
beklenen sonuç nedir? Gerçekleştirim hiç düşünülmez.

Yeşil adımda tek soru şudur: bu testi **nasıl** geçiririm? En kısa yol yeterlidir; tasarım
kaygısı bu adımda bir dikkat dağınıklığıdır.

Yeniden düzenleme adımında tek soru şudur: elde duran kod **nasıl daha iyi olur**? Bu
soruya verilen yanıtın doğru olup olmadığını testler bildirir.

Üç sorunun aynı anda düşünülmesi, sıradan bir programlama seansında sürekli yaşanan
şeydir ve maliyeti dikkat maliyetidir. Döngü bu maliyeti üçe bölerek düşürür.

#### Özet

- Test odaklı geliştirmede test bir doğrulama aracı değil tasarım aracıdır: işlevin adı,
  girdileri ve çıktısı onu kullanan testten türer.
- Kırmızı adımın güvencesi testin düşebildiğinin görülmesidir; yazıldığı anda yeşil olan
  test yanlış bir şeyi sınıyor olabilir.
- Yeşil adımın tek ölçütü testi geçirmektir; sabit değer döndüren sürüm, geçişin nedeninin
  yazılan satır olduğunu kanıtlar.
- Üçgenleme, ikinci bir örnekle gerçekleştirimi genelleşmeye zorlar; genelleştirme bir
  tahmin değil düşen testin dayatmasıdır.
- Yeniden düzenleme davranışı değiştirmeden iç yapıyı iyileştirir ve ancak yeşil bir test
  kümesinin üzerinde güvenli olur.

#### Sonraki Adım

Bu derste döngü iki kez döndü ve her turda tek bir kural ele alındı. Adım boyu bir seçimdir:
aynı iş üç kuralı birden kapsayan tek bir turla da yürütülebilirdi. O seçimin bedeli
ölçülebilir — düşen testin işaret ettiği yer, iki koşum arasında değişen satır sayısı ve
geri dönülecek son yeşil noktaya olan uzaklık. Sonraki ders aynı özelliği iki farklı adım
boyuyla kurar ve iki koşumu karşılaştırır.

### Küçük Adımlarla İlerleme

Source: https://academia.sh/tr/kurslar/birim-testi/kucuk-adimlarla-ilerleme

Aynı kural kümesine iki farklı adım boyuyla ulaşan yolların ölçülmesi, geri bildirim noktası sayısı ile hata arama alanının ilişkisi ve son yeşil noktaya dönüş maliyeti.

Önceki derste döngü iki kez döndü ve her turda tek bir kural ele alındı. Bu bir zorunluluk
değil, bir seçimdi: aynı iş, dört kuralı birden kapsayan tek bir turla da yürütülebilirdi.
Adım boyu test odaklı geliştirmenin en çok tartışılan ve en az ölçülen değişkenidir.

Bu ders adım boyunu ölçülebilir bir şeye bağlar. Aynı hedefe iki farklı yoldan gidilir ve
her durakta iki nicelik kaydedilir: kaç test düşüyor ve son duraktan bu yana kaç satır
değişti. İkinci nicelik önemlidir, çünkü bir test düştüğünde hatanın aranacağı alan tam
olarak odur.

#### Hedef: Dört Kural

Yenileme kuralları dörde çıktı. Yenileme son tarihi bugünden itibaren üye türünün ödünç
süresi kadar ileri alır ve yenileme sayacını artırır. Rezerve edilmiş bir kitap yenilenemez.
Bir ödünç en çok iki kez yenilenebilir. Süresi geçmiş bir ödünç yenilenemez; son gününde
yapılan yenileme ise kabul edilir.

```js
// 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,
  };
}
```

Dört kural yedi teste karşılık gelir. Sınır durumu — son gününde yapılan yenileme — ayrı
bir test olarak duruyor, çünkü karşılaştırma işlecinin yanlış seçilmesi tam orada
görünür.

```js
// yenileme.test.mjs — dort kuralin yedi testi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { yenile } from './yenileme.mjs';

const kayit = (ek = {}) => ({
  uyeNo: 'U-17', uyeTuru: 'ogrenci', sonGun: 1028, yenilemeSayisi: 0, rezerveEdildi: false, ...ek,
});

test('ogrenci yenilemesi bugunden yirmi sekiz gun ileri alir', () => {
  assert.equal(yenile(kayit(), 1020).sonGun, 1048);
});

test('standart uye yenilemesi bugunden on dort gun ileri alir', () => {
  assert.equal(yenile(kayit({ uyeTuru: 'uye' }), 1020).sonGun, 1034);
});

test('yenileme sayaci bir artar', () => {
  assert.equal(yenile(kayit(), 1020).yenilemeSayisi, 1);
});

test('rezerve edilmis kitap yenilenemez', () => {
  assert.throws(() => yenile(kayit({ rezerveEdildi: true }), 1020), /rezerve edilmis/);
});

test('en cok iki kez yenilenebilir', () => {
  assert.throws(() => yenile(kayit({ yenilemeSayisi: 2 }), 1020), /en cok 2 kez/);
});

test('gecikmis odunc yenilenemez', () => {
  assert.throws(() => yenile(kayit(), 1029), /gecikmis odunc/);
});

test('son gunde yapilan yenileme kabul edilir', () => {
  assert.equal(yenile(kayit(), 1028).sonGun, 1056);
});
```

```sh
node --test --test-reporter=tap yenileme.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ogrenci yenilemesi bugunden yirmi sekiz gun ileri alir
ok 2 - standart uye yenilemesi bugunden on dort gun ileri alir
ok 3 - yenileme sayaci bir artar
ok 4 - rezerve edilmis kitap yenilenemez
ok 5 - en cok iki kez yenilenebilir
ok 6 - gecikmis odunc yenilenemez
ok 7 - son gunde yapilan yenileme kabul edilir
# tests 7
# pass 7
# fail 0
```

Bu, varış noktasıdır. Soru oraya nasıl gidildiğidir.

#### İki Yolun Ölçümü

Aşağıdaki betik yolun aradaki duraklarını gerçekleştirim sürümleri olarak tutar. Büyük adım
yolunda iki durak vardır: ilk kuralın yazıldığı sürüm ve tamamlanmış sürüm. Küçük adım
yolunda beş durak vardır ve her durak bir kural ekler. Aynı yedi denetim her durakta
çalıştırılır; ayrıca ardışık iki durak arasında kaç gövde satırının değiştiği sayılır.

```js
// adim-boyu.mjs — ayni hedefe iki farkli adim boyuyla giden yolun olcumu
import assert from 'node:assert/strict';
import { yenile, KURALLAR, EN_COK_YENILEME, SINIR_ILETISI } from './yenileme.mjs';

function S1(kayit, bugun) {
  return {
    ...kayit,
    sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun,
  };
}

function S2(kayit, bugun) {
  return {
    ...kayit,
    sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun,
    yenilemeSayisi: kayit.yenilemeSayisi + 1,
  };
}

function S3(kayit, bugun) {
  if (kayit.rezerveEdildi) throw new Error('rezerve edilmis kitap yenilenemez');
  return {
    ...kayit,
    sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun,
    yenilemeSayisi: kayit.yenilemeSayisi + 1,
  };
}

function S4(kayit, bugun) {
  if (kayit.rezerveEdildi) throw new Error('rezerve edilmis kitap yenilenemez');
  if (kayit.yenilemeSayisi >= EN_COK_YENILEME) throw new Error(SINIR_ILETISI);
  return {
    ...kayit,
    sonGun: bugun + KURALLAR[kayit.uyeTuru].oduncGun,
    yenilemeSayisi: kayit.yenilemeSayisi + 1,
  };
}

function S5Hatali(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,
  };
}

const kayit = (ek = {}) => ({
  uyeNo: 'U-17', uyeTuru: 'ogrenci', sonGun: 1028, yenilemeSayisi: 0, rezerveEdildi: false, ...ek,
});

const TESTLER = {
  'ogrenci suresi': (y) => assert.equal(y(kayit(), 1020).sonGun, 1048),
  'uye suresi': (y) => assert.equal(y(kayit({ uyeTuru: 'uye' }), 1020).sonGun, 1034),
  'sayac artar': (y) => assert.equal(y(kayit(), 1020).yenilemeSayisi, 1),
  'rezerve reddi': (y) => assert.throws(() => y(kayit({ rezerveEdildi: true }), 1020), /rezerve edilmis/),
  'yenileme siniri': (y) => assert.throws(() => y(kayit({ yenilemeSayisi: 2 }), 1020), /en cok 2 kez/),
  'gecikme reddi': (y) => assert.throws(() => y(kayit(), 1029), /gecikmis odunc/),
  'son gun kabulu': (y) => assert.equal(y(kayit(), 1028).sonGun, 1056),
};

const TOPLAM = Object.keys(TESTLER).length;

function dusenler(surum) {
  return Object.entries(TESTLER).filter(([, t]) => {
    try {
      t(surum);
      return false;
    } catch {
      return true;
    }
  }).map(([ad]) => ad);
}

// Imza satiri sayilmaz; yalniz govde satirlari karsilastirilir.
function degisenSatir(eski, yeni) {
  const govde = (f) => f.toString().split('\n').slice(1).map((s) => s.trim()).filter(Boolean);
  const kalan = govde(eski);
  let eklenen = 0;
  for (const satir of govde(yeni)) {
    const i = kalan.indexOf(satir);
    if (i === -1) eklenen += 1;
    else kalan.splice(i, 1);
  }
  return eklenen + kalan.length;
}

const YOLLAR = { 'buyuk adim': [S1, yenile], 'kucuk adim': [S1, S2, S3, S4, yenile] };

for (const [ad, duraklar] of Object.entries(YOLLAR)) {
  console.log(ad);
  let enBuyuk = 0;
  duraklar.forEach((surum, i) => {
    const fark = i === 0 ? 0 : degisenSatir(duraklar[i - 1], surum);
    enBuyuk = Math.max(enBuyuk, fark);
    console.log(`  durak ${i + 1}  dusen ${dusenler(surum).length}/${TOPLAM}  degisen satir ${fark}`);
  });
  console.log(`  geri bildirim noktasi ${duraklar.length}, en buyuk arama alani ${enBuyuk} satir`);
}

const hataliFark = { 'buyuk adim': degisenSatir(S1, S5Hatali), 'kucuk adim': degisenSatir(S4, S5Hatali) };
console.log(`son adimda yanlis islec yazildiginda dusen test: ${dusenler(S5Hatali).join(', ')}`);
for (const [ad, fark] of Object.entries(hataliFark)) {
  console.log(`  ${ad}: hatanin aranacagi alan ${fark} satir`);
}
```

```
buyuk adim
  durak 1  dusen 4/7  degisen satir 0
  durak 2  dusen 0/7  degisen satir 4
  geri bildirim noktasi 2, en buyuk arama alani 4 satir
kucuk adim
  durak 1  dusen 4/7  degisen satir 0
  durak 2  dusen 3/7  degisen satir 1
  durak 3  dusen 2/7  degisen satir 1
  durak 4  dusen 1/7  degisen satir 1
  durak 5  dusen 0/7  degisen satir 1
  geri bildirim noktasi 5, en buyuk arama alani 1 satir
son adimda yanlis islec yazildiginda dusen test: son gun kabulu
  buyuk adim: hatanin aranacagi alan 4 satir
  kucuk adim: hatanin aranacagi alan 1 satir
```

#### Ölçümün Okunması

İki yol da aynı yere varıyor ve varış noktasında iki tablo da yedi testi geçiyor. Fark
yolda.

Büyük adım yolunda düşen test sayısı dörtten sıfıra bir sıçramada iniyor. Bu sıçramanın
içinde dört satırlık bir yazım var ve dört satırın hiçbiri tek başına doğrulanmadı.
Yazılanların dördü de doğruysa sorun yoktur; biri yanlışsa hangi satırın hangi testi
düşürdüğü, ancak elle ayıklanarak bulunur.

Küçük adım yolunda düşen test sayısı birer birer iniyor: dört, üç, iki, bir, sıfır. Her
inişin karşılığı tek satırlık bir yazımdır. Bu, ölçümün asıl bulgusudur — **arama alanı**
dört satırdan bir satıra iner.

Son üç satır bunu doğrudan gösteriyor. Gecikme denetimindeki karşılaştırma işleci yanlış
seçildiğinde — büyüktür yerine büyük eşit — yalnız bir test düşüyor: son gününde yapılan
yenilemenin kabulü. Düşen test iki yolda da aynıdır. Farklı olan, o testin işaret ettiği
alandır: büyük adımda dört satır, küçük adımda bir satır.

Buradan geri bildirim süresi kavramı çıkar. Geri bildirim süresi, bir yazım hatasının
yapıldığı an ile fark edildiği an arasındaki mesafedir; birimi saniye değil, **o aralıkta
yazılmış satır sayısıdır**. Adım boyu küçüldükçe bu mesafe kısalır.

#### Son Yeşil Noktaya Dönüş

Küçük adımın ikinci kazancı geri dönüş maliyetidir. Bir yazım beklenmedik biçimde
karmaşıklaşırsa en ucuz çözüm çoğu zaman onu atıp yeniden başlamaktır. Bu kararın maliyeti,
atılacak işin büyüklüğüdür.

Büyük adım yolunda son yeşil nokta birinci duraktır: geri dönüş, dört satırın tamamını
atmak demektir. Küçük adım yolunda son yeşil nokta her zaman bir satır geridedir. Aynı
şey, düşen testin yorumlanmasında da geçerlidir: küçük adımda "az önce yazdığım satır"
her zaman tek bir cümledir.

Bunun bir bedeli vardır ve tabloda görünüyor: küçük adım yolunda beş geri bildirim
noktası var, büyük adım yolunda iki. Her nokta bir koşum demektir. Koşum ucuzsa — dördüncü
derste ölçülen anlamda hızlı ve bağımsız testlerse — bu bedel önemsizdir. Koşum pahalıysa
adım boyu kendiliğinden büyür ve geri bildirim süresi uzar. İki dersin bağı budur: test
takımının hızı, geliştirme döngüsünün adım boyunu belirler.

#### Adım Boyunun Seçimi

Adım boyu sabit bir kural değil, belirsizliğe göre ayarlanan bir değişkendir. Yazılacak şey
tanıdıksa büyük adım ucuzdur ve ara duraklar zaman kaybıdır. Yazılacak şey belirsizse —
yeni bir alan, tanınmayan bir arayüz, karmaşık bir sınır durumu — adım küçülür.

Pratikte ölçüt şudur: bir adımın sonunda düşen test sayısı beklenenden farklı çıkıyorsa
adım fazla büyüktür. Yukarıdaki küçük adım tablosunda her durak tam olarak bir testi
yeşile çeviriyor; bu, adım boyunun kuralın tanesine oturduğunu gösterir.

#### Özet

- Adım boyu, iki geri bildirim noktası arasında yazılan satır sayısıyla ölçülür ve bir
  hatanın aranacağı alan tam olarak o sayıdır.
- Ölçümde büyük adım yolu düşen test sayısını dörtten sıfıra bir sıçramada indirdi; küçük
  adım yolu birer birer indirdi.
- Aynı yazım hatası iki yolda da tek bir testi düşürdü, ama arama alanı büyük adımda dört,
  küçük adımda bir satırdı.
- Küçük adımda son yeşil nokta her zaman bir satır geridedir; geri dönüş kararının maliyeti
  bu mesafedir.
- Küçük adımın bedeli daha çok koşumdur; test takımı yavaşsa adım boyu kendiliğinden büyür
  ve geri bildirim süresi uzar.

#### Sonraki Adım

Bu dersteki yedi test, kuralları ayrı ayrı cümlelerle anlattı ve her cümle bir kod bloğu
oldu. Kurallar çoğaldığında bu yazım biçimi tekrar üretir: aynı düzenek, aynı çağrı, yalnız
girdi ve beklenen sonuç değişir. Kuralın kendisi ise koda gömülü kalır ve kütüphane
görevlisiyle konuşurken okunamaz. Sonraki ders kuralları örnek tablolarına taşır, o
tablolar için bir biçim tanımlar ve tabloyu koşulabilir teste çeviren bir çözümleyici yazar.

### Davranış Odaklı Geliştirme

Source: https://academia.sh/tr/kurslar/birim-testi/davranis-odakli-gelistirme

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.

Ö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.

```bash
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.

```js
// 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.

```js
// 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.

```js
// 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`);
        }
      });
    }
  });
}
```

```sh
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.

```bash
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
```

```sh
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.

```js
// 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,
  };
}
```

```sh
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.

### Kabul Testi Odaklı Geliştirme

Source: https://academia.sh/tr/kurslar/birim-testi/kabul-testi-odakli-gelistirme

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ü.

Ö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.

```bash
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.

```js
// 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.

```js
// 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,
  };
}
```

```js
// 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 sahte bağımlılıklarla bir "dünya" kurar ve
adımları o dünya üzerinde yürütür.

```js
// 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

```sh
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.

```js
// 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']]);
});
```

```sh
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.

```js
// 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 };
      }
    },
  };
}
```

```sh
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 genişliktedir 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.

### Eski Kodu Test Altına Alma

Source: https://academia.sh/tr/kurslar/birim-testi/eski-kodu-test-altina-alma

Dikişsiz bir işlevin mevcut davranışının karakterizasyon testleriyle kayda geçirilmesi, parametre dikişi açılarak değiştirilebilir hâle getirilmesi ve aynı testlerin dikişten sonra da yeşil kalması.

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 bir işlev zaten oradadır,
davranışı yazılı değildir ve çoğu zaman kimse tam olarak ne yaptığını bilmez.

Böyle bir işlevi değiştirmenin sırası tersinedir: önce **ne yaptığı** kayda geçirilir, sonra
**değiştirilebilir** hâle getirilir. Birinci adımın aracı karakterizasyon testleridir,
ikincisininki dikiştir.

#### Dikişsiz Kod

Aşağıdaki işlev gecikme raporu üretir. Kayıtları modül düzeyinde tutar, günü sistem
saatinden okur ve sonucu doğrudan çıktı akışına yazar.

```js
// eski-rapor.mjs — yillardir calisan, testi olmayan surum
const KAYITLAR = [
  { kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1010, sube: 'merkez', iadeSubesi: 'merkez', iadeGunu: 1020 },
  { kitapNo: 'K-101', uyeNo: 'U-42', sonGun: 1024, sube: 'merkez', iadeSubesi: 'yamac', iadeGunu: 1024 },
  { kitapNo: 'K-555', uyeNo: 'U-58', sonGun: 1000, sube: 'yamac', iadeSubesi: 'yamac', iadeGunu: 1100 },
];

export function gecikmeRaporuYaz() {
  const bugun = Math.floor(Date.now() / 86400000);
  let toplam = 0;
  console.log(`gecikme raporu — gun ${bugun}`);
  for (const kayit of KAYITLAR) {
    const gecikme = kayit.iadeGunu - kayit.sonGun;
    let ucret = 0;
    if (gecikme > 3) ucret = (gecikme - 3) * 2;
    if (kayit.iadeSubesi !== kayit.sube) ucret += 5;
    if (ucret > 40) ucret = 40;
    toplam += ucret;
    console.log(`${kayit.kitapNo} ${kayit.uyeNo} gecikme ${gecikme} ucret ${ucret}`);
  }
  console.log(`toplam ${toplam}`);
}
```

Bir kod parçasında **dikiş** (seam), davranışı kaynağı değiştirilmeden
başkalaştırılabilen noktadır. Bu işlevde öyle bir nokta yoktur. Girdisi yoktur — kayıtlar içeridedir. Zamanı
dışarıdan alamaz — saati kendisi okur. Sonucu döndürmez — yazar. Beşinci dersteki ölçüm
tam olarak bu üç bağımlılığı adlandırmıştı; burada üçü aynı işlevde birleşmiştir.

Yine de bir gözlem noktası vardır: yazdığı satırlar. Küresel nesneler değiştirilerek hem
saat sabitlenebilir hem yazılanlar toplanabilir. Beşinci derste bu yolun pahalı olduğu
söylenmişti — burada geçici olarak tek yol odur.

```js
// gozlem.mjs — eski islevin bugunku ciktisi kayda geciriliyor
import { gecikmeRaporuYaz } from './eski-rapor.mjs';

const gercekNow = Date.now;
Date.now = () => 1020 * 86400000;
try {
  gecikmeRaporuYaz();
} finally {
  Date.now = gercekNow;
}
```

```
gecikme raporu — gun 1020
K-903 U-17 gecikme 10 ucret 14
K-101 U-42 gecikme 0 ucret 5
K-555 U-58 gecikme 100 ucret 40
toplam 59
```

İkinci satırda dikkat çeken bir şey var: K-101 kaydının gecikmesi sıfır, ücreti beş. Yedinci
derste bunun bir hata olduğu belirlenmişti — zamanında iade edilen kitaba şube dışı ücreti
uygulanıyor. Karakterizasyon testinin buradaki tavrı belirleyicidir.

#### Karakterizasyon Testleri

**Karakterizasyon testi** (characterization test), kodun ne yapması gerektiğini değil, şu
anda **ne yaptığını** kayda geçiren testtir. Beklenen değeri belirtimden almaz; koşumdan
alır. Amacı doğrulamak değil, ileride yapılacak değişikliğin fark edilmesini sağlamaktır.

Bu, birinci derste konulan kuralın kasıtlı bir istisnasıdır. Orada beklenen değerin sınanan
koddan bağımsız bir kaynaktan gelmesi gerektiği söylenmişti; burada beklenen değer doğrudan
sınanan koddan gelir. Fark amaçtadır: bu testler doğruluk iddiası taşımaz, bir **taban
çizgisi** kurar.

```js
// karakterizasyon.test.mjs — mevcut davranisi oldugu gibi kayda geciren testler
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gecikmeRaporuYaz } from './eski-rapor.mjs';

function yakala(bugun) {
  const gercekNow = Date.now;
  const gercekLog = console.log;
  const satirlar = [];
  Date.now = () => bugun * 86400000;
  console.log = (satir) => satirlar.push(satir);
  try {
    gecikmeRaporuYaz();
  } finally {
    Date.now = gercekNow;
    console.log = gercekLog;
  }
  return satirlar;
}

test('rapor basligi bugunun gun numarasini tasir', () => {
  assert.equal(yakala(1020)[0], 'gecikme raporu — gun 1020');
});

test('rapor satirlari bugunku haliyle uretilir', () => {
  assert.deepEqual(yakala(1020).slice(1), [
    'K-903 U-17 gecikme 10 ucret 14',
    'K-101 U-42 gecikme 0 ucret 5',
    'K-555 U-58 gecikme 100 ucret 40',
    'toplam 59',
  ]);
});

test('rapor icerigi bugunun gun numarasindan etkilenmez', () => {
  assert.deepEqual(yakala(2000).slice(1), yakala(1020).slice(1));
});
```

```sh
node --test --test-reporter=tap karakterizasyon.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - rapor basligi bugunun gun numarasini tasir
ok 2 - rapor satirlari bugunku haliyle uretilir
ok 3 - rapor icerigi bugunun gun numarasindan etkilenmez
# tests 3
# pass 3
# fail 0
```

İkinci test, bilinen hatayı da kayda geçiriyor: `K-101 U-42 gecikme 0 ucret 5` satırı
beklenen sonucun içinde. Bu bilerek yapılır. Karakterizasyon aşamasında hata düzeltilirse
iki değişiklik aynı anda yapılmış olur — yapı değişikliği ile davranış değişikliği
birbirine karışır ve düşen bir test hangisinden geldiğini söyleyemez.

Üçüncü test bir belirsizliği kapatıyor: rapor içeriğinin bugüne bağlı olmadığı, iki farklı
gün numarasıyla alınan sonuçların karşılaştırılmasıyla kayda geçiyor. Karakterizasyon
testleri yazılırken bu tür sorular sistematik olarak sorulur, çünkü bilinmeyen bağımlılık
sonraki adımı bozan şeydir.

#### Dikiş Açmak

Taban çizgisi kurulduktan sonra yapı değiştirilebilir. En ucuz dikiş türü **parametre
dikişidir**: içeriden okunan değer bir parametreye taşınır ve eski çağıranlar için o
parametreyi dolduran ince bir sarmalayıcı bırakılır.

```js
// eski-rapor.mjs — surum 2: parametre dikisi acildi, davranis aynen korundu
const KAYITLAR = [
  { kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1010, sube: 'merkez', iadeSubesi: 'merkez', iadeGunu: 1020 },
  { kitapNo: 'K-101', uyeNo: 'U-42', sonGun: 1024, sube: 'merkez', iadeSubesi: 'yamac', iadeGunu: 1024 },
  { kitapNo: 'K-555', uyeNo: 'U-58', sonGun: 1000, sube: 'yamac', iadeSubesi: 'yamac', iadeGunu: 1100 },
];

export function raporSatirlari(kayitlar, bugun) {
  const satirlar = [`gecikme raporu — gun ${bugun}`];
  let toplam = 0;
  for (const kayit of kayitlar) {
    const gecikme = kayit.iadeGunu - kayit.sonGun;
    let ucret = 0;
    if (gecikme > 3) ucret = (gecikme - 3) * 2;
    if (kayit.iadeSubesi !== kayit.sube) ucret += 5;
    if (ucret > 40) ucret = 40;
    toplam += ucret;
    satirlar.push(`${kayit.kitapNo} ${kayit.uyeNo} gecikme ${gecikme} ucret ${ucret}`);
  }
  satirlar.push(`toplam ${toplam}`);
  return satirlar;
}

export function gecikmeRaporuYaz() {
  const bugun = Math.floor(Date.now() / 86400000);
  for (const satir of raporSatirlari(KAYITLAR, bugun)) console.log(satir);
}
```

Üç şey aynı anda yapıldı: kayıtlar ve gün parametreye taşındı, çıktı yazma işi hesaptan
ayrıldı ve eski dış arayüz aynen korundu. Bu bir yeniden düzenlemedir — davranış
değişmemelidir. Doğrulaması, **hiç değiştirilmemiş** karakterizasyon testlerinin yeniden
koşturulmasıdır.

```sh
node --test --test-reporter=tap karakterizasyon.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - rapor basligi bugunun gun numarasini tasir
ok 2 - rapor satirlari bugunku haliyle uretilir
ok 3 - rapor icerigi bugunun gun numarasindan etkilenmez
# tests 3
# pass 3
# fail 0
```

Parametre dikişi tek seçenek değildir. **Nesne dikişi**, bağımlılığı bir nesne alanı olarak
verir — beşinci dersteki servis kurulumu bunun örneğidir. **Modül dikişi**, çağrılan modülün
kendisini değiştirilebilir kılar. Üçünün ortak yanı, davranışın kaynak değiştirmeden
başkalaştırılabildiği bir nokta yaratmalarıdır; farkı maliyet ve okunabilirliktedir.

#### Dikişten Geçen Testler

Dikiş açıldıktan sonra yazılan testler küresel nesnelere dokunmaz. Saat parametredir,
kayıtlar parametredir, sonuç dönüş değeridir.

```js
// dikis.test.mjs — dikisten gecen testler: saat ve kayitlar disaridan veriliyor
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { raporSatirlari } from './eski-rapor.mjs';

const kayit = (ek) => ({
  kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1010, sube: 'merkez', iadeSubesi: 'merkez', ...ek,
});

test('ucretsiz gun payi icindeki gecikme ucretlendirilmez', () => {
  const satirlar = raporSatirlari([kayit({ iadeGunu: 1013 })], 1020);

  assert.equal(satirlar[1], 'K-903 U-17 gecikme 3 ucret 0');
});

test('gecikmesiz sube disi iade bes birim ucretlendirilir', () => {
  const satirlar = raporSatirlari([kayit({ iadeGunu: 1010, iadeSubesi: 'yamac' })], 1020);

  assert.equal(satirlar[1], 'K-903 U-17 gecikme 0 ucret 5');
});

test('bos kayit listesi toplami sifir verir', () => {
  assert.deepEqual(raporSatirlari([], 1020), ['gecikme raporu — gun 1020', 'toplam 0']);
});
```

```sh
node --test --test-reporter=tap dikis.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - ucretsiz gun payi icindeki gecikme ucretlendirilmez
ok 2 - gecikmesiz sube disi iade bes birim ucretlendirilir
ok 3 - bos kayit listesi toplami sifir verir
# tests 3
# pass 3
# fail 0
```

İkinci testin adı bilinen hatayı **açıkça** adlandırıyor. Bu, karakterizasyon aşamasından
kalan mirasın son hâlidir: davranış artık gizli bir yan etki değil, adı konmuş ve sınanan
bir kural. Düzeltme kararı verildiğinde yapılacak iş bellidir — testin adı ve beklentisi
değişir, sonra gerçekleştirim; ve bu artık üçüncü dersteki sıradan kırmızı–yeşil turudur.

Sıra da bu yüzden önemlidir. Karakterizasyon testleri olmadan yapılan bir dikiş açma
işleminde, davranışın korunup korunmadığını söyleyen hiçbir şey yoktur. Testler önce yazılır,
yapı sonra değişir, düzeltme en sonda gelir.

#### Özet

- Dikiş, bir kodun davranışının kaynağı değiştirilmeden başkalaştırılabildiği noktadır;
  dikişsiz kodda tek gözlem noktası küresel nesneler olabilir.
- Karakterizasyon testi kodun ne yapması gerektiğini değil şu anda ne yaptığını kayda geçirir;
  beklenen değeri belirtimden değil koşumdan alır.
- Bilinen hatalar da taban çizgisine dahil edilir; düzeltme ile yapı değişikliği aynı anda
  yapılmaz.
- Parametre dikişi içeriden okunan değeri parametreye taşır ve eski arayüzü ince bir
  sarmalayıcıyla korur; nesne dikişi ve modül dikişi aynı amacın başka maliyetli biçimleridir.
- Dikişten geçen testler küresel nesnelere dokunmaz; sıra her zaman karakterizasyon, dikiş,
  düzeltme biçimindedir.

#### Kurs Kapanışı

Bu kurs iki konuda tek bir soruyu iki yönden ele aldı: bir birim testi neyi kanıtlar ve o
kanıt nasıl güvenilir tutulur?

Birim Test Pratiği konusu testi bir ölçüm aracına çevirdi. Testin gövdesi hazırla, uygula ve
doğrula bölümlerine ayrıldı; iddianın sinyal gücü yakaladığı bozulma sayısıyla, adlandırmanın
değeri düşen koşumun taşıdığı bilgiyle, bağımsızlık sıra permütasyonuyla, düzenek maliyeti
satır sayısıyla, kapsam üç ayrı birimin elle sayımıyla ve kırılganlık yeniden düzenleme
karşısında düşen test sayısıyla ölçüldü. Sahte bağımlılıkların beş türü elle yazıldı ve her
birinin yanıtladığı soru ayrıldı.

Test Odaklı Geliştirme konusu sırayı tersine çevirdi. Kırmızı–yeşil–yeniden düzenle döngüsü
adım adım koşturuldu, adım boyunun geri bildirim süresine etkisi arama alanıyla ölçüldü,
kurallar alan diliyle yazılmış örnek tablolarına taşındı, kabul ölçütleri dış halka olarak
kuruldu ve son olarak testi olmayan bir kod karakterizasyon testleriyle taban çizgisine
oturtulup dikişle açıldı.

Kursun boyunca sessizce taşınan bir varsayım var ve adının konması gerekiyor. Buradaki her
test **süreç içinde** ve **gerçek bağımlılık olmadan** koştu. Katalog bir eşlemeydi, depo
bir taklit nesneydi, bildirim bir casus nesneydi, saat bir işlevdi. Bu sahte bağımlılıkların
gerçeğine uyduğu varsayıldı — ama hiçbir yerde sınanmadı. Taklit katalog gerçek veri
tabanının kısıtlarını taşımıyor; casus bildirim, gerçek bildirim kanalının başarısızlıklarını
üretmiyor; sürücünün kurduğu dünya, gerçek bir dağıtımda karşılaşılacak sınırların hiçbirini
içermiyor.

Bu varsayımın sınanması ayrı bir test katmanının konusudur. Tümleşim, Sözleşme ve Uçtan Uca
Test kursu bu sınırları gerçek çalıştırarak ele alır: hangi bağımlılığın gerçeği kullanılır,
sahte bağımlılık ile gerçeğin aynı sözleşmeye uyduğu nasıl güvenceye alınır ve uygulamanın
tamamı üzerinden koşan testler nasıl kararlı tutulur.

