---
title: 'Tümleşim, Sözleşme ve Uçtan Uca Test'
source: 'https://academia.sh/tr/kurslar/tumlesim-testi'
language: tr
updated: '2026-08-17T04:42:56+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Dış bağımlılıklarla çalışan testleri yalıtımlı biçimde kurar'
    - 'Sözleşme testleriyle servisler arası uyumu güvenceye alır'
    - 'Uçtan uca testleri kararlı ve bakımı yapılabilir tutar'
---

# Tümleşim, Sözleşme ve Uçtan Uca Test

Birim testin sahte bağımlılıklarla kurduğu dünyanın gerçeğe uyup uymadığının sınanması: hangi sınırın gerçek çalıştırılacağının karara bağlanması, test ortamının ve verisinin yönetimi, veritabanı ile göç doğrulaması, dış servisin kayıt ve tekrar oynatmayla taklidi, tüketici güdümlü sözleşme testiyle servisler arası uyumun güvenceye alınması ve uygulamanın tamamı üzerinden koşan testlerin kararlı, bakımı yapılabilir tutulması.

## Tümleşim Testleri

Gerçek çalıştırılan sınırın seçilmesi ve bedelinin ödenmesi: hangi bağımlılığın gerçeğiyle koşturulacağının karara bağlanması, bağımlı servisleri ayağa kaldıran ortamın yönetimi, üretim benzeri test verisinin hazırlanması ve maskelenmesi, göç ile şema doğrulamasının teste alınması ve dış servisin kayıt ile tekrar oynatma yaklaşımlarıyla taklit edilmesi.

### Tümleşim Testinin Kapsamı

Source: https://academia.sh/tr/kurslar/tumlesim-testi/tumlesim-testinin-kapsami

Hangi bağımlılığın gerçeğiyle koşturulacağının bir karar olarak ele alınması: aynı testin sahte ve gerçek katalogla koşturulması, şema uyuşmazlığının yalnız gerçek sınırda yakalanması ve sınırın bedelinin sorgu sayısıyla ölçülmesi.

Birim Testi ve Test Odaklı Geliştirme kursunun kapanışında bir varsayımın adı konmuştu:
oradaki her test süreç içinde ve gerçek bağımlılık olmadan koştu, sahte bağımlılıkların
gerçeğine uyduğu varsayıldı ama hiçbir yerde sınanmadı. Bu kurs o varsayımı sınar. İlk
soru, sınamanın nereden başlayacağıdır.

Yanıt bir erdem değil, bir karardır. "Gerçeğiyle test etmek daha güvenli" cümlesi hiçbir
şey seçmez; her bağımlılık için gerçeğin çalıştırılması ayrı bir bedel getirir ve ayrı bir
kusur sınıfını görünür kılar. Bu ders kararı bir örnek üzerinde verir ve üç şeyi birlikte
ölçer: seçilen sınırın **yakaladığı** kusur sınıfı, **yakalamadığı** kusur sınıfı ve
**bedeli**. Kurs boyunca ölçü ekseni budur.

#### Sınır Kararı

Ödünç servisinin dört bağımlılığı vardı: kayıtları tutan katalog, üyeye haber veren
bildirim kanalı, işlem günlüğü ve gün numarasını veren saat. Her biri için iki seçenek
bulunur — sahte bağımlılık ya da gerçeği — ve dördü bağımsız seçildiğinde on altı
yapılandırma çıkar. Bunların hepsi anlamlı değildir. Saatin gerçeği testte hiçbir kusur
sınıfı açığa çıkarmaz, yalnız belirlenimciliği yok eder. Bildirim kanalının gerçeği bir
süreç ayağa kaldırmayı gerektirir. Katalog ise ikisinin arasındadır: gerçeği süreç içinde
çalışabilen bir veri tabanıdır ve kendi kısıtlarını taşır.

Bu ders katalog sınırını gerçek çalıştırır, kalanını sahte bırakır. Bu yapılandırmanın adı
**tümleşim testidir** (integration test): sınanan kod ile en az bir bağımlılığın gerçek
gerçekleştirimi birlikte koşar.

#### Aynı Testin İki Yapılandırması

Ödünç servisi, önceki kurstan geldiği biçimiyle duruyor.

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

export function oduncServisiKur({ katalog, bildirim, 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);
      return kayit;
    },
  };
}
```

Katalog arayüzünün iki gerçekleştirimi yazılır. Birincisi önceki kurstaki taklit nesnedir.
İkincisi aynı arayüzü gerçek bir şema üzerinde karşılar; şema, üretimde kullanılan sürümdür
ve kataloğu yazan ekipten gelir.

```js
// kataloglar.mjs — ayni katalog arayuzunun iki gerceklestirimi: taklit nesne ve node:sqlite
import { DatabaseSync } from 'node:sqlite';

export const SEMA = `
CREATE TABLE odunc (
  kitap_no  TEXT PRIMARY KEY,
  uye_no    TEXT NOT NULL,
  sube      TEXT NOT NULL,
  alis_gunu INTEGER NOT NULL,
  son_gun   INTEGER NOT NULL,
  CHECK (son_gun > alis_gunu)
);`;

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),
    sorguSayisi: () => 0,
  };
}

export function sqliteKatalog(yol = ':memory:') {
  const db = new DatabaseSync(yol);
  db.exec(SEMA);
  let sorgu = 0;
  const calistir = (metin, ...bagli) => { sorgu += 1; return db.prepare(metin).run(...bagli); };
  const oku = (metin, ...bagli) => { sorgu += 1; return db.prepare(metin).get(...bagli); };
  return {
    uyeOduncSayisi: (uyeNo) => oku('SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?', uyeNo).n,
    ekle: (kayit) => calistir(
      'INSERT INTO odunc (kitap_no, uye_no, sube, alis_gunu, son_gun) VALUES (?, ?, ?, ?, ?)',
      kayit.kitapNo, kayit.uyeNo, kayit.sube ?? null, kayit.alisGunu, kayit.sonGun),
    bul: (kitapNo) => {
      const s = oku('SELECT kitap_no, uye_no, alis_gunu, son_gun FROM odunc WHERE kitap_no = ?', kitapNo);
      return s && { kitapNo: s.kitap_no, uyeNo: s.uye_no, alisGunu: s.alis_gunu, sonGun: s.son_gun };
    },
    sil: (kitapNo) => calistir('DELETE FROM odunc WHERE kitap_no = ?', kitapNo),
    sorguSayisi: () => sorgu,
  };
}
```

Düzenek hangi gerçekleştirimin kurulacağını tek bir yerden seçer. Bildirim bir casus
nesnedir, saat bir işlevdir; ikisi de önceki kurstaki biçimini korur.

```js
// duzenek.mjs — ayni testin iki yapilandirmasi: katalog sahte ya da gercek
import { oduncServisiKur } from './servis.mjs';
import { taklitKatalog, sqliteKatalog } from './kataloglar.mjs';

export function duzenekKur(yapilandirma) {
  const katalog = yapilandirma === 'gercek' ? sqliteKatalog() : taklitKatalog();
  const gonderilen = [];
  const bildirim = { gonder: (uyeNo, metin) => gonderilen.push({ uyeNo, metin }) };
  const servis = oduncServisiKur({ katalog, bildirim, saat: () => 1000 });
  return { servis, katalog, gonderilen };
}

export const UYE = { no: 'U-17', tur: 'uye', sube: 'merkez' };
```

Testler tek dosyadır ve yapılandırmayı bir ortam değişkeninden alır. Üçüncü test, sınır
denetimini sınadığı için kataloğa doğrudan beş kayıt yazar; o kayıtları yazan taraf şemayı
bildiğinden `sube` alanını verir.

```js
// odunc.test.mjs — ayni uc test; KATALOG degiskeni sinirin gercek calisip calismadigini secer
import test from 'node:test';
import assert from 'node:assert/strict';
import { duzenekKur, UYE } from './duzenek.mjs';

const yapilandirma = process.env.KATALOG ?? 'sahte';

test('yeni odunc katalogda gorunur', () => {
  const { servis, katalog } = duzenekKur(yapilandirma);
  servis.oduncVer(UYE, 'K-903');
  assert.equal(katalog.bul('K-903').sonGun, 1014);
});

test('iade edilen kitap katalogdan duser', () => {
  const { servis, katalog } = duzenekKur(yapilandirma);
  servis.oduncVer(UYE, 'K-903');
  servis.iadeAl('K-903');
  assert.equal(katalog.bul('K-903'), undefined);
});

test('odunc siniri asilinca hata verilir', () => {
  const { servis, katalog } = duzenekKur(yapilandirma);
  for (let i = 0; i < 5; i += 1) {
    katalog.ekle({ kitapNo: `K-90${i}`, uyeNo: UYE.no, sube: 'merkez', alisGunu: 1000, sonGun: 1014 });
  }
  assert.throws(() => servis.oduncVer(UYE, 'K-999'), /odunc siniri asildi: 5/);
});
```

#### Sahte Sınırda Yeşil

Önceki kursun bıraktığı yapılandırma budur: katalog bir taklit nesnedir.

```sh
KATALOG=sahte node --test --test-reporter=tap odunc.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 - odunc siniri asilinca hata verilir
# tests 3
# pass 3
# fail 0
```

Üç test de geçiyor. Bu koşum, servisin doğru olduğunu değil, servisin **taklit kataloğun
kabul ettiği** her şeyi yaptığını gösterir. Taklit katalog bir eşlemedir ve bir eşleme her
biçimdeki kaydı kabul eder.

#### Gerçek Sınırda Kırmızı

Tek değişiklik, katalog sınırının gerçeğiyle koşturulmasıdır. Test dosyası, iddialar ve
servis aynen duruyor.

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

```
not ok 1 - yeni odunc katalogda gorunur
not ok 2 - iade edilen kitap katalogdan duser
ok 3 - odunc siniri asilinca hata verilir
# tests 3
# pass 1
# fail 2
```

İlk iki test düştü. Düşme nedeni koşumun ayrıntısında yazılıdır.

```sh
KATALOG=gercek node --test --test-reporter=tap odunc.test.mjs | grep "error:" | sort -u
```

```
  error: 'NOT NULL constraint failed: odunc.sube'
```

Yakalanan kusur sınıfının adı **şema uyuşmazlığıdır**: servisin ürettiği kayıt, gerçek
şemanın zorunlu tuttuğu bir alanı taşımıyor. Bu kusur koddaydı ve önceki kursun tamamı
boyunca oradaydı; taklit katalog onu göremezdi, çünkü taklit nesnenin kabul kuralı yoktur.
Üçüncü testin yeşil kalması da bilgi taşır: kayıtları şemayı bilerek yazan taraf hatasızdır,
kusur yalnız servisin yazdığı yoldadır.

Düzeltme servisin kayıt biçimindedir.

```js
// servis.mjs — kayit artik sube alanini da tasiyor
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export function oduncServisiKur({ katalog, bildirim, 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, sube: uye.sube,
        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);
      return kayit;
    },
  };
}
```

```sh
KATALOG=gercek node --test --test-reporter=tap odunc.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 - odunc siniri asilinca hata verilir
# tests 3
# pass 3
# fail 0
```

#### Bu Testin Görmediği

Katalog sınırı gerçek çalışıyor, bildirim sınırı hâlâ bir casus nesne. Bu, bir kusur
sınıfını dokunulmadan bırakır. Bildirimin muhatabına üye numarası yerine üye türünü yazan
bir sürüm ele alınsın.

```js
// kusurlu.mjs — bildirimin muhatabina uye numarasi yerine uye turu yazan surum
import { KURALLAR } from './servis.mjs';

export function kusurluServisKur({ katalog, bildirim, saat }) {
  return {
    oduncVer(uye, kitapNo) {
      const kural = KURALLAR[uye.tur];
      const bugun = saat();
      const kayit = { uyeNo: uye.no, kitapNo, sube: uye.sube, alisGunu: bugun, sonGun: bugun + kural.oduncGun };
      katalog.ekle(kayit);
      bildirim.gonder(uye.tur, `son tarih: ${kayit.sonGun}`);
      return kayit;
    },
  };
}
```

Test, bildirimi de doğruluyor: bir bildirim gitmiş olmalı ve metni doğru son günü
taşımalı.

```js
// kacan.test.mjs — katalog gercek, bildirim casus nesne; muhatap kusuru sinanmis olmuyor
import test from 'node:test';
import assert from 'node:assert/strict';
import { sqliteKatalog } from './kataloglar.mjs';
import { kusurluServisKur } from './kusurlu.mjs';

test('odunc kaydi yazilir ve bir bildirim gonderilir', () => {
  const katalog = sqliteKatalog();
  const gonderilen = [];
  const bildirim = { gonder: (muhatap, metin) => gonderilen.push({ muhatap, metin }) };
  const servis = kusurluServisKur({ katalog, bildirim, saat: () => 1000 });

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

  assert.equal(katalog.bul('K-903').uyeNo, 'U-17');
  assert.equal(gonderilen.length, 1);
  assert.match(gonderilen[0].metin, /son tarih: 1014/);
  console.log(`gonderilen muhatap: ${gonderilen[0].muhatap}`);
});
```

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

```
# gonderilen muhatap: uye
ok 1 - odunc kaydi yazilir ve bir bildirim gonderilir
# tests 1
# pass 1
# fail 0
```

Test yeşil, muhatap yanlış. Kaçan kusur sınıfının adı **muhatap kimliği uyuşmazlığıdır**:
gerçek bildirim kanalı tanımadığı bir muhatap için hata döndürürdü, casus nesne ise
kendisine verilen her değeri kaydeder. Bu sınıfın yakalanması bildirim sınırının da gerçek
çalıştırılmasını gerektirir; katalog sınırını gerçek yapmak buna hiçbir katkı sağlamaz.
Buradan çıkan kural, kursun tamamına yayılır: bir testin gördüğü kusur sınıfı, gerçek
çalıştırdığı sınırla belirlenir.

#### Bedel

Gerçek sınırın karşılığı ölçülebilir. İki nicelik koşumdan bağımsızdır — koşum başına
çalışan sorgu sayısı ve düzeneğin satır sayısı — üçüncüsü, süre, makineye bağlıdır ve ham
milisaniye yerine karşılaştırma olarak bildirilir.

```js
// bedel.mjs — iki yapilandirmanin sorgu sayisi, duzenek satiri ve goreli suresi
import { duzenekKur, UYE } from './duzenek.mjs';
import { taklitKatalog, sqliteKatalog } from './kataloglar.mjs';

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

function olc(yapilandirma) {
  const ilk = duzenekKur(yapilandirma);
  ilk.servis.oduncVer(UYE, 'K-903');
  ilk.servis.iadeAl('K-903');
  const baslangic = performance.now();
  for (let i = 0; i < TUR; i += 1) {
    const { servis } = duzenekKur(yapilandirma);
    servis.oduncVer(UYE, 'K-903');
    servis.iadeAl('K-903');
  }
  return { sure: performance.now() - baslangic, sorgu: ilk.katalog.sorguSayisi() };
}

const sahte = olc('sahte');
const gercek = olc('gercek');

console.log(`sahte katalog : ${TUR} tur, tur basina ${sahte.sorgu} sorgu, katalog ${satir(taklitKatalog)} satir`);
console.log(`gercek katalog: ${TUR} tur, tur basina ${gercek.sorgu} sorgu, katalog ${satir(sqliteKatalog)} satir`);
console.log(`gercek sinir daha uzun surdu      : ${gercek.sure > sahte.sure}`);
console.log(`sure farki en az on kat            : ${gercek.sure / sahte.sure >= 10}`);
```

```
sahte katalog : 500 tur, tur basina 0 sorgu, katalog 8 satir
gercek katalog: 500 tur, tur basina 4 sorgu, katalog 17 satir
gercek sinir daha uzun surdu      : true
sure farki en az on kat            : true
```

Bir ödünç ile bir iade turu, gerçek sınırda dört sorgu çalıştırıyor; sahte sınırda hiç
sorgu yok. Katalog gerçekleştirimi sekiz satırdan on yedi satıra çıkıyor ve bu on yedi
satır artık bakım gerektiren bir düzenek kodudur: şema değişirse burası da değişir. Süre
farkının mutlak değeri makineye ve dosya sistemine bağlıdır, yönü değildir.

Bu üç sayı kararı taşır. Dört sorgu ve on kat süre, bir kusur sınıfının — şema
uyuşmazlığının — karşılığında ödenmiştir. Aynı bedeli saat sınırı için ödemek hiçbir kusur
sınıfı kazandırmaz; bu yüzden saat sahte kalır.

#### Özet

- Hangi sınırın gerçek çalıştırılacağı bağımlılık başına verilen bir karardır; dört
  bağımlılık on altı yapılandırma üretir ve hepsi anlamlı değildir.
- Taklit kataloğun kabul kuralı yoktur; gerçek şemanın vardır. Önceki kurs boyunca yeşil
  kalan servis, gerçek şemayla iki testte düştü ve kusurun adı şema uyuşmazlığıdır.
- Bir testin göremediği kusur sınıfı, gerçek çalıştırmadığı sınırla belirlenir: bildirim
  casus nesne kaldığı için muhatap kimliği uyuşmazlığı yeşil kaldı.
- Sınırın bedeli koşumdan bağımsız niceliklerle ölçülür: tur başına dört sorgu ve dokuz
  satırlık ek düzenek kodu.
- Bu kursun her dersi aynı üçlüyü yazar: yakalanan kusur sınıfı, kaçan kusur sınıfı ve
  bedel.

#### Sonraki Adım

Katalog sınırı süreç içinde gerçek çalıştı, çünkü kataloğun gerçeği aynı süreçte
yaşayabiliyordu. Bildirim kanalının gerçeği yaşayamaz: ayrı bir süreçtir, bir bağlantı
noktası dinler ve testten önce ayağa kalkmış, testten sonra indirilmiş olması gerekir.
Ayrıca kataloğun kendisi de bellekte değil bir dosyada durursa, şemanın o dosyada kurulmuş
olması bir kurulum adımıdır. Sonraki ders bu adımları test ortamının yönetimi olarak ele
alır: bağımlı servis nasıl ayağa kaldırılır, hazır olduğu nasıl anlaşılır ve bu düzeneğin
her koşumda ödenen bedeli kaç süreç, kaç adım eder.

### Test Ortamı Yönetimi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/test-ortami-yonetimi

Bağımlı servisin testten önce ayağa kaldırılması: kurulum ve yıkım adımlarının sayılması, hazır olma yoklamasının bir adım olarak kurulması, önceki dersin kaçan kusur sınıfının gerçek sınırda yakalanması ve süreç ömrüne bağlı kusurun neden kaçtığının gösterilmesi.

Önceki derste katalog sınırı gerçek çalıştı, çünkü kataloğun gerçeği aynı süreçte
yaşayabiliyordu. Bildirim kanalının gerçeği yaşayamaz: ayrı bir süreçtir, bir bağlantı
noktası dinler ve kendi verisini okur. Bu, testin çevresine bir **test ortamı** (test
environment) koymayı gerektirir — testten önce kurulan, testten sonra yıkılan ve her
koşumda bedeli yeniden ödenen bir düzenek.

Bu ders o düzeneği adım adım kurar ve adımları sayar. Kurulan ortam, önceki dersin kaçan
kusur sınıfını — muhatap kimliği uyuşmazlığını — yakalar. Kaçırdığı sınıf da aynı ölçüde
belirgindir ve gösterilir.

#### Bağımlı Servis

Bildirim kanalı, muhatabı üye tablosunda arayan küçük bir servistir. Tanımadığı bir
muhatap için 404 döndürür, tanıdığı için 202 ile kuyruğa alır. Sağlık ucu, ortamın hazır
olup olmadığını sorabilmesi içindir.

```js
// bildirim-servisi.mjs — bagimli servis: uye tablosunu okur, bilinmeyen muhataba 404 doner
import { createServer } from 'node:http';
import { DatabaseSync } from 'node:sqlite';

export function servisKur(bagl, katalogYolu) {
  const db = new DatabaseSync(katalogYolu);
  const kuyruk = [];
  const sunucu = createServer((istek, yanit) => {
    if (istek.url === '/saglik') {
      yanit.writeHead(200, { 'content-type': 'application/json' });
      return yanit.end('{"durum":"hazir"}');
    }
    if (istek.method === 'GET' && istek.url === '/bildirim') {
      yanit.writeHead(200, { 'content-type': 'application/json' });
      return yanit.end(JSON.stringify({ sayi: kuyruk.length }));
    }
    let govde = '';
    istek.on('data', (p) => { govde += p; });
    istek.on('end', () => {
      const { muhatap, metin } = JSON.parse(govde);
      const uye = db.prepare('SELECT uye_no FROM uye WHERE uye_no = ?').get(muhatap);
      if (uye === undefined) {
        yanit.writeHead(404, { 'content-type': 'application/json' });
        return yanit.end(`{"hata":"bilinmeyen muhatap: ${muhatap}"}`);
      }
      kuyruk.push({ muhatap, metin });
      yanit.writeHead(202, { 'content-type': 'application/json' });
      yanit.end('{"durum":"kuyruga alindi"}');
    });
  });
  sunucu.listen(bagl, '127.0.0.1');
  return sunucu;
}

if (process.argv[2] !== undefined) servisKur(Number(process.argv[2]), process.argv[3]);
```

#### Ortamın Adımları

Ortam dört adımdan oluşur ve adımların sırası bağlayıcıdır: geçici dizin açılır, şema ile
tohum verisi yazılır, süreç başlatılır, hazır olma yoklaması yapılır. Yıkım iki adımdır.

```js
// ortam.mjs — test ortami: gecici dizin, sema, bagimli surec ve hazir olma yoklamasi
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';

export const ADIMLAR = ['gecici dizin', 'sema ve tohum verisi', 'surec baslatma', 'hazir olma yoklamasi'];
export const SEMA = `
CREATE TABLE uye (uye_no TEXT PRIMARY KEY, sube TEXT NOT NULL);
CREATE TABLE odunc (
  kitap_no TEXT PRIMARY KEY, uye_no TEXT NOT NULL, sube TEXT NOT NULL,
  alis_gunu INTEGER NOT NULL, son_gun INTEGER NOT NULL);
INSERT INTO uye VALUES ('U-17', 'merkez');`;

async function hazirBekle(taban, enCok = 100) {
  for (let deneme = 1; deneme <= enCok; deneme += 1) {
    try {
      const y = await fetch(`${taban}/saglik`);
      if (y.ok) { await y.text(); return deneme; }
    } catch { /* surec henuz dinlemiyor */ }
    await new Promise((c) => setTimeout(c, 20));
  }
  throw new Error('bagimli servis hazir olmadi');
}

export async function ortamKur(bagl) {
  const kok = mkdtempSync(join(tmpdir(), 'ortam-'));
  const katalogYolu = join(kok, 'katalog.db');
  const db = new DatabaseSync(katalogYolu);
  db.exec(SEMA);
  db.close();
  const taban = `http://127.0.0.1:${bagl}`;
  const baslat = () => spawn(process.execPath, ['bildirim-servisi.mjs', String(bagl), katalogYolu],
    { stdio: 'ignore' });
  let surec = baslat();
  await hazirBekle(taban);
  return {
    taban, katalogYolu, surecSayisi: 1,
    yenidenBaslat: async () => {
      surec.kill('SIGTERM');
      await new Promise((c) => setTimeout(c, 200));
      surec = baslat();
      await hazirBekle(taban);
    },
    kapat: () => { surec.kill('SIGTERM'); rmSync(kok, { recursive: true, force: true }); },
  };
}
```

Dördüncü adım gereksiz görünebilir: süreç başlatıldıysa hazırdır. Değildir. Süreç
başlatmak, sürecin bağlantı noktasını dinlemeye başlaması demek değildir; ikisinin arasında
yorumlayıcının açılması, modüllerin yüklenmesi ve veri tabanının açılması vardır. Yoklama
atlandığında ne olduğu ölçülebilir.

```js
// hazirsiz.mjs — hazir olma yoklamasi atlandiginda surec baslatma ile ilk istek yarisir
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { SEMA } from './ortam.mjs';

const kok = mkdtempSync(join(tmpdir(), 'hazirsiz-'));
const yol = join(kok, 'katalog.db');
const db = new DatabaseSync(yol);
db.exec(SEMA);
db.close();

const surec = spawn(process.execPath, ['bildirim-servisi.mjs', '8934', yol], { stdio: 'ignore' });
try {
  await fetch('http://127.0.0.1:8934/saglik');
  console.log('yoklamasiz ilk istek: yanit alindi');
} catch (hata) {
  console.log(`yoklamasiz ilk istek: ${hata.cause?.code ?? hata.code}`);
}
surec.kill('SIGTERM');
rmSync(kok, { recursive: true, force: true });
```

```
yoklamasiz ilk istek: ECONNREFUSED
```

Bu, ortam yönetiminin kendi kusur sınıfıdır ve adı **hazır olma yarışıdır**. Yoklama
yerine sabit bir bekleme konması sorunu çözmez, yalnız gizler: bekleme kısa kaldığında test
düşer, uzun tutulduğunda her koşum o süreyi öder. Yoklama, süreyi ortamın kendisine
sordurur.

#### Kaçan Sınıfın Yakalanması

Bildirim artık bir casus nesne değil, gerçek bir istemcidir.

```js
// istemci.mjs — bildirim kanalinin gercek gerceklestirimi: yapilandirilan adrese gider
export function bildirimIstemcisi(taban) {
  return {
    gonder: async (muhatap, metin) => {
      const yanit = await fetch(`${taban}/bildirim`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ muhatap, metin }),
      });
      if (yanit.status !== 202) {
        const govde = await yanit.json();
        throw new Error(`bildirim reddedildi: ${yanit.status} ${govde.hata}`);
      }
    },
  };
}
```

Ödünç servisinin iki sürümü sınanır: önceki dersin muhataba üye türü yazan kusurlu sürümü
ile üye numarası yazan düzeltilmiş sürümü. Bildirim çağrısı süreç dışına çıktığı için
`oduncVer` artık eşzamansızdır; bu, sınırın kendisinden gelen bir sonuçtur.

```js
// servisler.mjs — odunc servisinin iki surumu: muhataba uye numarasi ya da uye turu yazan
import { DatabaseSync } from 'node:sqlite';

export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } };

export function dosyaKatalogu(yol) {
  const db = new DatabaseSync(yol);
  return {
    ekle: (k) => db.prepare('INSERT INTO odunc VALUES (?, ?, ?, ?, ?)')
      .run(k.kitapNo, k.uyeNo, k.sube, k.alisGunu, k.sonGun),
    bul: (kitapNo) => db.prepare('SELECT uye_no FROM odunc WHERE kitap_no = ?').get(kitapNo),
  };
}

function servisKur({ katalog, bildirim, saat }, muhatabiSec) {
  return {
    async oduncVer(uye, kitapNo) {
      const bugun = saat();
      const kayit = {
        uyeNo: uye.no, kitapNo, sube: uye.sube,
        alisGunu: bugun, sonGun: bugun + KURALLAR[uye.tur].oduncGun,
      };
      katalog.ekle(kayit);
      await bildirim.gonder(muhatabiSec(uye), `son tarih: ${kayit.sonGun}`);
      return kayit;
    },
  };
}

export const kusurluServis = (b) => servisKur(b, (uye) => uye.tur);
export const duzeltilmisServis = (b) => servisKur(b, (uye) => uye.no);
```

Test, ortamı kurulum ve yıkım kancalarına bağlar. Ortam bir kez kurulur, tek test onu
kullanır ve yıkım her durumda çalışır.

```js
// ortam.test.mjs — bildirim siniri gercek calisiyor; SERVIS degiskeni hangi surumun sinandigini secer
import test, { before, after } from 'node:test';
import assert from 'node:assert/strict';
import { ortamKur } from './ortam.mjs';
import { bildirimIstemcisi } from './istemci.mjs';
import { dosyaKatalogu, kusurluServis, duzeltilmisServis } from './servisler.mjs';

const UYE = { no: 'U-17', tur: 'uye', sube: 'merkez' };
let ortam;

before(async () => { ortam = await ortamKur(8931); });
after(() => ortam.kapat());

test('odunc verildiginde bildirim kabul edilir', async () => {
  const kur = process.env.SERVIS === 'duzeltilmis' ? duzeltilmisServis : kusurluServis;
  const servis = kur({
    katalog: dosyaKatalogu(ortam.katalogYolu),
    bildirim: bildirimIstemcisi(ortam.taban),
    saat: () => 1000,
  });
  await servis.oduncVer(UYE, 'K-903');
  const kuyruk = await (await fetch(`${ortam.taban}/bildirim`)).json();
  assert.equal(kuyruk.sayi, 1);
});
```

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

```
not ok 1 - odunc verildiginde bildirim kabul edilir
  error: 'bildirim reddedildi: 404 bilinmeyen muhatap: uye'
# tests 1
# pass 0
# fail 1
```

Önceki derste bu kusur yeşil kalmıştı: casus nesne, kendisine verilen her muhatabı kabul
ediyordu. Gerçek servis kabul etmiyor ve reddin gerekçesini yazıyor. Düzeltilmiş sürüm
aynı ortamda geçiyor.

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

```
ok 1 - odunc verildiginde bildirim kabul edilir
# tests 1
# pass 1
# fail 0
```

#### Bu Ortamın Görmediği

Ortam süreci bir kez ayağa kaldırır ve testten sonra indirir. Aradaki tüm koşum, tek bir
süreç ömrü içinde geçer. Bu, bir kusur sınıfını dokunulmadan bırakır: kabul edilen
bildirimler yalnız bellekte durmaktadır.

```js
// kalicilik.mjs — testin yesil kaldigi durum ile surecin yeniden baslamasindan sonraki durum
import { ortamKur } from './ortam.mjs';

const ortam = await ortamKur(8932);
const gonder = (muhatap) => fetch(`${ortam.taban}/bildirim`, {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ muhatap, metin: 'son tarih: 1014' }),
});
const kuyruk = async () => (await (await fetch(`${ortam.taban}/bildirim`)).json()).sayi;

await gonder('U-17');
console.log(`test yesilken kuyruk               : ${await kuyruk()}`);
await ortam.yenidenBaslat();
console.log(`surec yeniden baslatildiktan sonra : ${await kuyruk()}`);
console.log(`katalog dosyasi ayakta kaldi       : ${ortam.katalogYolu.endsWith('katalog.db')}`);
ortam.kapat();
```

```
test yesilken kuyruk               : 1
surec yeniden baslatildiktan sonra : 0
katalog dosyasi ayakta kaldi       : true
```

Test yeşil, kuyruk uçucu. Kaçan kusur sınıfının adı **süreç ömrüne bağlı durumdur**:
bağımlılığın verisi yeniden başlatmayı geçirmiyor ve tek ömürlü bir ortam bunu hiçbir
koşumda sormaz. Bu sınıfın yakalanması, ortama beşinci bir adım — bağımlılığı koşum
ortasında yeniden başlatma — eklemeyi gerektirir; o adımın bedeli de her koşumda ödenir.

#### Bedel

Ortamın bedeli üç koşumdan bağımsız nicelikle ve bir süre karşılaştırmasıyla yazılır; kat
sayısının mutlak değeri makineye ve dosya sistemine bağlıdır, farkın yönü değildir.

```js
// bedel.mjs — gercek sinirin bedeli: surec, kurulum adimi ve casus nesneye gore sure
import { ortamKur, ADIMLAR } from './ortam.mjs';
import { bildirimIstemcisi } from './istemci.mjs';

const N = 100;
const ortam = await ortamKur(8933);
const gercek = bildirimIstemcisi(ortam.taban);
const casus = { gonder: async () => {} };

async function olc(bildirim) {
  const baslangic = performance.now();
  for (let i = 0; i < N; i += 1) await bildirim.gonder('U-17', 'son tarih: 1014');
  return performance.now() - baslangic;
}

await olc(gercek);
const casusSure = await olc(casus);
const gercekSure = await olc(gercek);
ortam.kapat();

console.log(`ayaga kalkan surec  : ${ortam.surecSayisi}`);
console.log(`kurulum adimi       : ${ADIMLAR.length} (${ADIMLAR.join(', ')})`);
console.log(`yikim adimi         : 2 (surec sonlandirma, gecici dizin silme)`);
console.log(`kosum basina dosya  : 1 (katalog dosyasi)`);
console.log(`${N} bildirimde gercek sinir daha uzun surdu : ${gercekSure > casusSure}`);
console.log(`sure farki en az yirmi kat                 : ${gercekSure / casusSure >= 20}`);
```

```
ayaga kalkan surec  : 1
kurulum adimi       : 4 (gecici dizin, sema ve tohum verisi, surec baslatma, hazir olma yoklamasi)
yikim adimi         : 2 (surec sonlandirma, gecici dizin silme)
kosum basina dosya  : 1 (katalog dosyasi)
100 bildirimde gercek sinir daha uzun surdu : true
sure farki en az yirmi kat                 : true
```

Bir süreç, dört kurulum adımı, iki yıkım adımı ve koşum başına bir dosya. Bu altı adımın
her biri bakım gerektirir: şema değiştiğinde ikinci adım, servisin başlatma arayüzü
değiştiğinde üçüncü adım, sağlık ucu değiştiğinde dördüncü adım yeniden yazılır.

Aynı adımları yalıtılmış bir **kap** (container) içinde yürüten bir araç sınıfı vardır.
Kap, adımların sayısını azaltmaz — geçici dizin yerine geçici bir kap, süreç başlatma
yerine kap başlatma gelir — ama iki şeyi değiştirir: bağımlılığın sürümü ve yapılandırması
koşumdan koşuma aynı kalır ve bağımlılığın yeniden başlatılması ucuzlar. Yukarıdaki kaçan
kusur sınıfı da tam olarak orada yakalanabilir hâle gelir. Karşılığında her koşum bir imaj
çekme ya da önbellekten okuma bedeli öder.

#### Özet

- Test ortamı, testin çevresinde kurulan ve yıkılan bir düzenektir; burada dört kurulum ve
  iki yıkım adımından oluşuyor.
- Hazır olma yoklaması bir adımdır: atlandığında ilk istek bağlantı reddiyle döner ve
  yerine konan sabit bekleme ya kırılganlık ya da sabit gecikme üretir.
- Gerçek bildirim sınırı, önceki dersin kaçan kusur sınıfını yakaladı: bilinmeyen muhatap
  404 ile reddedildi ve test kırmızıya döndü.
- Tek ömürlü bir ortam, süreç ömrüne bağlı durumu göremez; kabul edilen bildirimler yeniden
  başlatmadan sonra yok oldu, test yine de yeşildi.
- Bedel bir süreç, altı adım ve koşum başına bir dosyadır; kap tabanlı bir araç sınıfı bu
  adımları ortadan kaldırmaz, sürüm kararlılığı ve ucuz yeniden başlatma karşılığında yer
  değiştirir.

#### Sonraki Adım

Bu ortamın şeması ile birlikte yazılan tohum verisi tek satırdı: bir üye, tek şube. Testler
geçti, çünkü sorulan sorular tek satırla yanıtlanabiliyordu. Üretimdeki katalog böyle
değildir — üye başına düşen ödünç sayısı geniş bir dağılım gösterir, kayıtlar birbirine
bağlıdır ve kısıtlar yalnız belirli bileşimlerde ihlal edilir. Sonraki ders test verisinin
kendisini ele alır: üretim benzeri veri hangi özelliğini korumak zorundadır, kişisel alanlar
maskelendiğinde hangi özellik bozulur ve tek satırlık bir tohumun göremediği kusur sınıfı
nedir.

### Test Verisi Yönetimi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/test-verisi-yonetimi

Üretim benzeri test verisinin hangi özelliğini korumak zorunda olduğunun ölçülmesi: hacmin değil dağılım kuyruğunun yakaladığı rapor kusuru, maskelemenin bozduğu kısıt, dağılım ve ilişki özellikleri ve maskelenen değerin yok ettiği kusur sınıfı.

Önceki dersin ortamı tek satırlık bir tohum veriyle kurulmuştu: bir üye, bir şube. Testler
geçti, çünkü sorulan sorular tek satırla yanıtlanabiliyordu. Üretimdeki katalog böyle
değildir ve fark yalnız hacim değildir.

Bu dersin sorusu şudur: test verisinin **hangi özelliği** korunmak zorundadır? Yanıt
"üretime benzesin" değildir, çünkü bu ölçülemez. Ölçülebilir olan üç özellik vardır —
dağılım, kısıt ve ilişki — ve her biri farklı bir kusur sınıfını görünür kılar. Kişisel
alanların maskelenmesi bu üç özelliğin hangisini bozuyorsa, o özelliğe bağlı kusur sınıfını
da beraberinde götürür.

#### Veri Üreteci

Üreteç, önceki kurstan gelen doğrusal eşlenik üreteçle tohumdan türetilir; tohum görünür
olduğu sürece aynı veri yeniden üretilebilir. `kip` bağımsız değişkeni yalnız bir şeyi
değiştirir: bir üyenin birden çok geciken ödüncü olup olamayacağını.

```js
// uretec.mjs — tohumdan turetilen katalog verisi; kip dagilimin kuyrugunu belirler
export const SEMA = `
CREATE TABLE uye (
  uye_no TEXT PRIMARY KEY, ad TEXT NOT NULL,
  eposta TEXT NOT NULL UNIQUE, sube TEXT NOT NULL);
CREATE TABLE odunc (
  kitap_no TEXT PRIMARY KEY, uye_no TEXT NOT NULL REFERENCES uye(uye_no),
  alis_gunu INTEGER NOT NULL, son_gun INTEGER NOT NULL, iade_gunu INTEGER);`;

export function uretecKur(tohum) {
  let durum = tohum;
  return () => { durum = (durum * 1103515245 + 12345) % 2147483648; return durum / 2147483648; };
}

const ADLAR = ['Ayse', 'Mehmet', 'Zeynep', 'Emre', 'Elif', 'Kerem', 'Deniz', 'Sinem'];
const SOYADLAR = ['Yilmaz, Kara', 'Demir', 'Sahin', 'Koc', 'Aydin'];
const SUBELER = ['merkez', 'kampus', 'sanat'];

export function veriUret(tohum, uyeSayisi, kip = 'kuyruklu') {
  const rast = uretecKur(tohum);
  const uyeler = [];
  const oduncler = [];
  let kitapNo = 0;
  for (let i = 0; i < uyeSayisi; i += 1) {
    const no = `U-${String(i).padStart(4, '0')}`;
    uyeler.push({
      no,
      ad: `${ADLAR[Math.floor(rast() * ADLAR.length)]} ${SOYADLAR[Math.floor(rast() * SOYADLAR.length)]}`,
      eposta: `uye${i}@ornek.gecersiz`,
      sube: SUBELER[Math.floor(rast() * SUBELER.length)],
    });
    const kuyrukta = rast() > 0.85;
    const adet = kuyrukta ? 6 + Math.floor(rast() * 5) : 1 + Math.floor(rast() * 2);
    let gecikmis = 0;
    for (let k = 0; k < adet; k += 1) {
      const alis = 900 + Math.floor(rast() * 80);
      // duz kipte her uyenin en cok bir geciken odunci olur
      const gecikti = rast() > 0.55 && (kip === 'kuyruklu' || gecikmis === 0);
      if (gecikti) gecikmis += 1;
      oduncler.push({
        kitapNo: `K-${String(kitapNo += 1).padStart(5, '0')}`,
        uyeNo: no,
        alisGunu: alis,
        sonGun: alis + 14,
        iadeGunu: gecikti ? null : alis + 5,
      });
    }
  }
  return { uyeler, oduncler };
}
```

Sınanan iş, geciken ödüncü olan üyeleri sayan rapordur. Kusurlu sürüm üyeleri değil ödünç
satırlarını sayar. Beklenen sonuç, sorgudan bağımsız olarak üretilen veriden hesaplanır.

```js
// yukle.mjs — uretilen veriyi node:sqlite'a yukler ve geciken uye raporunu iki surumde sorar
import { DatabaseSync } from 'node:sqlite';
import { SEMA, veriUret } from './uretec.mjs';

export const BUGUN = 1014;

export function veritabaniKur({ uyeler, oduncler }, yol = ':memory:') {
  const db = new DatabaseSync(yol);
  db.exec('PRAGMA foreign_keys = ON;');
  db.exec(SEMA);
  const uyeEkle = db.prepare('INSERT INTO uye VALUES (?, ?, ?, ?)');
  const oduncEkle = db.prepare('INSERT INTO odunc VALUES (?, ?, ?, ?, ?)');
  let sorgu = 0;
  for (const u of uyeler) { uyeEkle.run(u.no, u.ad, u.eposta, u.sube); sorgu += 1; }
  for (const o of oduncler) {
    oduncEkle.run(o.kitapNo, o.uyeNo, o.alisGunu, o.sonGun, o.iadeGunu);
    sorgu += 1;
  }
  return { db, sorgu };
}

const KUSURLU = `SELECT COUNT(*) AS uye_sayisi FROM uye u
  JOIN odunc o ON o.uye_no = u.uye_no
  WHERE o.iade_gunu IS NULL AND o.son_gun < ?`;
const DUZELTILMIS = KUSURLU.replace('COUNT(*)', 'COUNT(DISTINCT u.uye_no)');

export function gecikenUyeSayisi(db, bugun, surum) {
  return db.prepare(surum === 'duzeltilmis' ? DUZELTILMIS : KUSURLU).get(bugun).uye_sayisi;
}

// test kahini: sayim SQL'den bagimsiz olarak uretilen veriden hesaplanir
export function beklenenGecikenUye({ oduncler }, bugun) {
  const kume = new Set();
  for (const o of oduncler) if (o.iadeGunu === null && o.sonGun < bugun) kume.add(o.uyeNo);
  return kume.size;
}

export const VERI_KUMELERI = {
  minimal: () => ({
    uyeler: [{ no: 'U-0000', ad: 'Ayse Demir', eposta: 'uye0@ornek.gecersiz', sube: 'merkez' }],
    oduncler: [{ kitapNo: 'K-00001', uyeNo: 'U-0000', alisGunu: 900, sonGun: 914, iadeGunu: null }],
  }),
  duz: () => veriUret(20260731, 200, 'duz'),
  kuyruklu: () => veriUret(20260731, 200, 'kuyruklu'),
};
```

#### Hacim Değil, Kuyruk

İki üretilmiş kümenin hacmi birebir aynıdır. Ayrıldıkları tek yer, üye başına düşen geciken
ödünç sayısının dağılımıdır.

```js
// dagilim.mjs — ayni hacimde iki kume: biri kuyruklu, digeri duz
import { VERI_KUMELERI, BUGUN } from './yukle.mjs';

for (const ad of ['duz', 'kuyruklu']) {
  const veri = VERI_KUMELERI[ad]();
  const sayac = new Map();
  for (const o of veri.oduncler) {
    if (o.iadeGunu === null && o.sonGun < BUGUN) sayac.set(o.uyeNo, (sayac.get(o.uyeNo) ?? 0) + 1);
  }
  const gecikenOdunc = [...sayac.values()].reduce((a, b) => a + b, 0);
  const enCok = Math.max(...sayac.values());
  console.log(`${ad.padEnd(8)}: ${veri.uyeler.length} uye, ${veri.oduncler.length} odunc, ` +
    `${sayac.size} geciken uye, ${gecikenOdunc} geciken odunc, uye basina en cok ${enCok}`);
}
```

```
duz     : 200 uye, 572 odunc, 143 geciken uye, 143 geciken odunc, uye basina en cok 1
kuyruklu: 200 uye, 572 odunc, 143 geciken uye, 297 geciken odunc, uye basina en cok 8
```

Aynı test üç kümede koşturulur.

```js
// rapor.test.mjs — ayni rapor testi uc veri kumesinde; VERI ve RAPOR degiskenleriyle secilir
import test from 'node:test';
import assert from 'node:assert/strict';
import { veritabaniKur, gecikenUyeSayisi, beklenenGecikenUye, VERI_KUMELERI, BUGUN } from './yukle.mjs';

const veriAdi = process.env.VERI ?? 'minimal';
const surum = process.env.RAPOR ?? 'kusurlu';

test(`geciken uye sayisi dogru (${veriAdi})`, () => {
  const veri = VERI_KUMELERI[veriAdi]();
  const { db } = veritabaniKur(veri);
  assert.equal(gecikenUyeSayisi(db, BUGUN, surum), beklenenGecikenUye(veri, BUGUN));
});
```

```sh
for v in minimal duz kuyruklu; do
  VERI=$v RAPOR=kusurlu node --test --test-reporter=tap rapor.test.mjs | grep -E '^ *(ok|not ok)'
done
VERI=kuyruklu RAPOR=duzeltilmis node --test --test-reporter=tap rapor.test.mjs | grep -E '^ *(ok|not ok)'
```

```
ok 1 - geciken uye sayisi dogru (minimal)
ok 1 - geciken uye sayisi dogru (duz)
not ok 1 - geciken uye sayisi dogru (kuyruklu)
ok 1 - geciken uye sayisi dogru (kuyruklu)
```

İkinci satır bu dersin ana ölçümüdür. Düz küme, minimal kümenin iki yüz katı büyüklüğünde
olduğu hâlde kusuru göremedi; yakalayan şey hacim değil, üye başına birden çok geciken
ödüncün bulunmasıydı. Yakalanan kusur sınıfının adı **dağılıma bağlı kusurdur** ve
korunması gereken özellik, dağılımın kuyruğudur. Düzeltme `COUNT(*)` yerine
`COUNT(DISTINCT u.uye_no)` yazmaktır; dördüncü satır düzeltilmiş sürümün aynı kümede
geçtiğini gösteriyor.

#### Maskelemenin Bozduğu Özellik

Üretim verisi teste taşınırken kişisel alanlar maskelenir. Maskeleme biçimi, hangi
özelliğin ayakta kaldığını belirler. Dört biçim aynı kaynak küme üzerinde denenir.

```js
// maskeleme.mjs — dort maskeleme bicimi ve her birinin bozdugu ozellik
import { veritabaniKur, VERI_KUMELERI } from './yukle.mjs';

const kaynak = VERI_KUMELERI.kuyruklu();

const BICIMLER = {
  'sabit eposta  ': (v) => ({ ...v, uyeler: v.uyeler.map((u) => ({ ...u, eposta: 'gizli@ornek.gecersiz' })) }),
  'sabit sube    ': (v) => ({ ...v, uyeler: v.uyeler.map((u) => ({ ...u, sube: 'gizli' })) }),
  'yeni uye no   ': (v) => ({ ...v, uyeler: v.uyeler.map((u) => ({ ...u, no: `M-${u.no.slice(2)}` })) }),
  'turetilmis    ': (v) => ({
    ...v,
    uyeler: v.uyeler.map((u) => ({
      ...u, ad: `Uye-${u.no.slice(2)}`, eposta: `u${u.no.slice(2)}@ornek.gecersiz`,
    })),
  }),
};

const subeSayisi = (db) => db.prepare('SELECT COUNT(DISTINCT sube) AS n FROM uye').get().n;

console.log(`kaynak         : yuklendi, ${subeSayisi(veritabaniKur(kaynak).db)} ayri sube`);
for (const [ad, maske] of Object.entries(BICIMLER)) {
  try {
    const { db } = veritabaniKur(maske(kaynak));
    console.log(`${ad} : yuklendi, ${subeSayisi(db)} ayri sube`);
  } catch (hata) {
    console.log(`${ad} : yuklenemedi, ${hata.message}`);
  }
}
```

```
kaynak         : yuklendi, 3 ayri sube
sabit eposta   : yuklenemedi, UNIQUE constraint failed: uye.eposta
sabit sube     : yuklendi, 1 ayri sube
yeni uye no    : yuklenemedi, FOREIGN KEY constraint failed
turetilmis     : yuklendi, 3 ayri sube
```

Üç bozulma üç ayrı özelliğe karşılık gelir. Sabit e-posta maskesi **kısıtı** bozar: teklik
kısıtı, veri tabanına yüklenmeyi bile engelliyor. Sabit şube maskesi **dağılımı** bozar:
veri yükleniyor, ama şube kırılımlı her rapor artık tek şube görüyor ve şubeye bağlı hiçbir
kusur sınıfı sınanamıyor. Üye numarasının yalnız bir tabloda değiştirilmesi **ilişkiyi**
bozar: yabancı anahtar kısıtı yüklemeyi durduruyor, kısıt kapalı olsaydı birleştirme
sessizce boş dönerdi. Türetilmiş maske üçünü de korur, çünkü maskelenen değer kaynak
değerin tekliğinden türetiliyor ve tabloların tamamına aynı biçimde uygulanıyor.

#### Bu Verinin Görmediği

Türetilmiş maske üç özelliği de korudu, ama maskelenen alanın **içeriğini** korumadı.
Rapor dışa aktarılırken alanlar virgülle birleştiriliyor ve tırnaklanmıyor.

```js
// disaaktar.mjs — geciken uye raporunu virgulle ayrilmis satirlara cevirir; alanlar tirnaklanmaz
import { veritabaniKur, VERI_KUMELERI, BUGUN } from './yukle.mjs';

const RAPOR = `SELECT u.uye_no, u.ad, u.sube, COUNT(*) AS gecikme FROM uye u
  JOIN odunc o ON o.uye_no = u.uye_no
  WHERE o.iade_gunu IS NULL AND o.son_gun < ?
  GROUP BY u.uye_no, u.ad, u.sube ORDER BY u.uye_no`;

export function disaAktar(satirlar) {
  return satirlar.map((s) => [s.uye_no, s.ad, s.sube, s.gecikme].join(','));
}

export function raporSatirlari(bicim) {
  const kaynak = VERI_KUMELERI.kuyruklu();
  const veri = bicim === 'maskeli'
    ? { ...kaynak, uyeler: kaynak.uyeler.map((u) => ({ ...u, ad: `Uye-${u.no.slice(2)}` })) }
    : kaynak;
  const { db } = veritabaniKur(veri);
  return db.prepare(RAPOR).all(BUGUN);
}
```

```js
// disaaktar.test.mjs — disa aktarilan her satir dort alan tasimali; veri bicimi degiskenle secilir
import test from 'node:test';
import assert from 'node:assert/strict';
import { disaAktar, raporSatirlari } from './disaaktar.mjs';

const bicim = process.env.VERI_BICIMI ?? 'maskeli';

test(`disa aktarilan satirlar dort alan tasir (${bicim})`, () => {
  const satirlar = disaAktar(raporSatirlari(bicim));
  const bozuk = satirlar.filter((s) => s.split(',').length !== 4);
  assert.deepEqual(bozuk.slice(0, 1), []);
});
```

```sh
VERI_BICIMI=maskeli node --test --test-reporter=tap disaaktar.test.mjs | grep -E '^ *(ok|not ok)'
VERI_BICIMI=kaynak node --test --test-reporter=tap disaaktar.test.mjs | grep -E '^ *(ok|not ok)'
node -e "const m = await import('./disaaktar.mjs'); console.log(m.disaAktar(m.raporSatirlari('kaynak')).find((s) => s.split(',').length !== 4));" --input-type=module
```

```
ok 1 - disa aktarilan satirlar dort alan tasir (maskeli)
not ok 1 - disa aktarilan satirlar dort alan tasir (kaynak)
U-0004,Elif Yilmaz, Kara,sanat,1
```

Maskelenmiş veriyle test yeşil; aynı test kaynak veriyle kırmızı. Kaçan kusur sınıfının adı
**içeriğe bağlı kusurdur**: kusuru tetikleyen şey ad alanının içindeki virgüldür ve
türetilmiş maske o virgülü yok etti. Bu, maskelemenin kaçınılmaz bedelidir — kişisel alanın
içeriği testte tutulamaz. Karşılığı, içerik biçimini ayrı bir kural listesiyle sınamaktır:
maskelenmemiş, üretilmiş ve ayrılıcı içeren adlar dışa aktarma testine el ile eklenir.

#### Bedel

```js
// bedel.mjs — uretim benzeri verinin bedeli: satir, sorgu, dosya boyutu ve goreli sure
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { veritabaniKur, VERI_KUMELERI } from './yukle.mjs';

const kok = mkdtempSync(join(tmpdir(), 'veri-'));
const ozet = (veri) => createHash('sha256').update(JSON.stringify(veri)).digest('hex').slice(0, 12);

function olc(ad) {
  const veri = VERI_KUMELERI[ad]();
  const yol = join(kok, `${ad}.db`);
  const baslangic = performance.now();
  const { sorgu } = veritabaniKur(veri, yol);
  const sure = performance.now() - baslangic;
  return { satir: veri.uyeler.length + veri.oduncler.length, sorgu, sure, boyut: statSync(yol).size, ozet: ozet(veri) };
}

const minimal = olc('minimal');
const kuyruklu = olc('kuyruklu');
rmSync(kok, { recursive: true, force: true });

console.log(`minimal  : ${String(minimal.satir).padStart(3)} satir, ${String(minimal.sorgu).padStart(3)} yukleme sorgusu, ${minimal.boyut} bayt`);
console.log(`kuyruklu : ${kuyruklu.satir} satir, ${kuyruklu.sorgu} yukleme sorgusu, ${kuyruklu.boyut} bayt`);
console.log(`tohum 20260731 ayni ozeti veriyor : ${ozet(VERI_KUMELERI.kuyruklu()) === kuyruklu.ozet}`);
console.log(`kuyruklu kume daha uzun yuklendi  : ${kuyruklu.sure > minimal.sure}`);
```

```
minimal  :   2 satir,   2 yukleme sorgusu, 24576 bayt
kuyruklu : 772 satir, 772 yukleme sorgusu, 77824 bayt
tohum 20260731 ayni ozeti veriyor : true
kuyruklu kume daha uzun yuklendi  : true
```

Yedi yüz yetmiş iki yükleme sorgusu ve yetmiş sekiz kilobaytlık bir dosya, her koşumda yeniden
ödenir. Bu bedelin karşılığı tek bir kusur sınıfıdır: dağılıma bağlı kusur. Aynı bedeli düz
kümeyle ödemek o sınıfı kazandırmadı; hacim tek başına bir ölçüt değildir. Tohumun görünür
olması bedeli sabitler — dosyada saklanan bir veri kümesi yerine sekiz haneli bir sayı taşınır
ve aynı veri yeniden üretilir.

#### Özet

- Test verisinin üç ölçülebilir özelliği vardır: dağılım, kısıt ve ilişki; her biri ayrı
  bir kusur sınıfını görünür kılar.
- Aynı hacimdeki iki kümeden yalnız kuyruklu olan rapor kusurunu yakaladı; iki yüz katı
  büyüklükteki düz küme kusuru göremedi.
- Sabit değerli maske teklik kısıtını, sabit alan maskesi dağılımı, tek tabloda yapılan
  yeniden numaralandırma ilişkiyi bozar; türetilmiş maske üçünü de korur.
- Türetilmiş maske alanın içeriğini koruyamaz: ad içindeki ayrılıcıya bağlı dışa aktarma
  kusuru maskelenmiş veriyle yeşil, kaynak veriyle kırmızı kaldı.
- Bedel yedi yüz yetmiş iki yükleme sorgusu ve yetmiş sekiz kilobayttır; tohum görünür
  olduğu sürece veri kümesi taşınmaz, yeniden üretilir.

#### Sonraki Adım

Buradaki bütün ölçümler tek bir şemayı varsaydı: tablolar hazır, sütunlar yerinde, kısıtlar
tanımlı. Şema kendi başına bir ürün parçasıdır ve zaman içinde değişir — sütun eklenir,
sütun bölünür, kısıt sıkılaştırılır. Bu değişiklikler **göç** adı verilen sıralı adımlarla
uygulanır ve her adımın bir ileri, bir de geri yönü vardır. Sonraki ders göçün kendisini
teste alır: adımların sırası bozulduğunda ne olur, geri göç veriyi koruyabiliyor mu ve
şemanın beklenen biçimde olduğu nasıl doğrulanır.

### Veritabanı Testleri

Source: https://academia.sh/tr/kurslar/tumlesim-testi/veritabani-testleri

Göç adımlarının ve şemanın teste alınması: ileri göçün ürettiği şeklin doğrulanması, göç numarasının tekilliği, geri göçte veri kaybının yakalanması, göçün var olan veri üzerinde kısıt ihlaliyle düşmesi ve şema testinin göremediği geri doldurma değeri.

Önceki derste şema sabit bir varsayımdı: tablolar hazır, sütunlar yerinde, kısıtlar
tanımlı. Şema kendi başına bir ürün parçasıdır ve sıralı adımlarla değişir. Bu adımların
her birinin bir ileri, bir de geri yönü vardır ve ikisi de kod kadar kırılabilir.

Bu ders veri tabanını öğretmez; **göçü ve şemayı teste alır**. Sorular şunlardır: ileri göç
beklenen şekli üretiyor mu, adımların numaralandırması sağlam mı, geri göç veriyi koruyor
mu ve göç var olan veri üzerinde ne yapıyor. Bütün ölçümler gerçek bir `node:sqlite`
veritabanı üzerinde koşar.

#### Göç Kümesi

Dört adım vardır. Üçüncü adımın geri yönü iki sürümde verilir; sınanacak olan budur.

```js
// gocler.mjs — sirali goc adimlari; ucuncu adimin geri yonu iki surumde verilir
export const GERI_SURUMU = process.env.GERI_GOC ?? 'kusurlu';

export const GOCLER = [
  {
    no: 1,
    ad: 'baslangic semasi',
    ileri: [
      `CREATE TABLE uye (uye_no TEXT PRIMARY KEY, ad TEXT NOT NULL, sube TEXT NOT NULL)`,
      `CREATE TABLE odunc (kitap_no TEXT PRIMARY KEY, uye_no TEXT NOT NULL,
         alis_gunu INTEGER NOT NULL, son_gun INTEGER NOT NULL)`,
    ],
    geri: [`DROP TABLE odunc`, `DROP TABLE uye`],
  },
  {
    no: 2,
    ad: 'odunce sube sutunu',
    ileri: [
      `ALTER TABLE odunc ADD COLUMN sube TEXT`,
      `UPDATE odunc SET sube = 'merkez'`,
    ],
    geri: [`ALTER TABLE odunc DROP COLUMN sube`],
  },
  {
    no: 3,
    ad: 'uye adinin bolunmesi',
    ileri: [
      `ALTER TABLE uye ADD COLUMN soyad TEXT`,
      `UPDATE uye SET soyad = substr(ad, instr(ad, ' ') + 1),
         ad = substr(ad, 1, instr(ad, ' ') - 1) WHERE instr(ad, ' ') > 0`,
    ],
    geri: GERI_SURUMU === 'duzeltilmis'
      ? [`UPDATE uye SET ad = ad || ' ' || soyad WHERE soyad IS NOT NULL AND soyad <> ''`,
         `ALTER TABLE uye DROP COLUMN soyad`]
      : [`ALTER TABLE uye DROP COLUMN soyad`],
  },
  {
    no: 4,
    ad: 'ad ve soyad teklik kisiti',
    ileri: [`CREATE UNIQUE INDEX uye_ad_soyad ON uye(ad, soyad)`],
    geri: [`DROP INDEX uye_ad_soyad`],
  },
];
```

Koşucu üç iş yapar: numara dizisini denetler, uygulanan adımları bir kayıt tablosunda tutar
ve çalıştırdığı sorguları sayar. Sayaç bedelin ölçüsüdür.

```js
// kosucu.mjs — goc kosucusu: numara dizisini denetler, uygulanani kaydeder, sorgu sayar
import { DatabaseSync } from 'node:sqlite';
import { GOCLER } from './gocler.mjs';

export function sirayiDenetle(gocler) {
  const numaralar = gocler.map((g) => g.no);
  const tekil = new Set(numaralar);
  if (tekil.size !== numaralar.length) {
    const yinelenen = numaralar.find((n, i) => numaralar.indexOf(n) !== i);
    throw new Error(`goc numarasi yineleniyor: ${yinelenen}`);
  }
  const sirali = [...tekil].sort((a, b) => a - b);
  sirali.forEach((n, i) => {
    if (n !== i + 1) throw new Error(`goc numarasinda bosluk: ${i + 1} eksik`);
  });
  return sirali.length;
}

export function veritabaniAc(yol = ':memory:') {
  const db = new DatabaseSync(yol);
  db.exec('CREATE TABLE IF NOT EXISTS goc_kaydi (no INTEGER PRIMARY KEY)');
  db.sorgu = 0;
  return db;
}

export const uygulanan = (db) =>
  db.prepare('SELECT no FROM goc_kaydi ORDER BY no').all().map((s) => s.no);

export function ileri(db, hedef) {
  sirayiDenetle(GOCLER);
  for (const goc of GOCLER.filter((g) => g.no <= hedef)) {
    const kayitli = uygulanan(db);
    if (kayitli.includes(goc.no)) continue;
    if (goc.no > 1 && !kayitli.includes(goc.no - 1)) {
      throw new Error(`goc sirasi bozuk: ${goc.no} oncesinde ${goc.no - 1} uygulanmamis`);
    }
    for (const komut of goc.ileri) { db.exec(komut); db.sorgu += 1; }
    db.prepare('INSERT INTO goc_kaydi VALUES (?)').run(goc.no);
    db.sorgu += 1;
  }
  return db;
}

export function geri(db, hedef) {
  for (const goc of [...GOCLER].reverse().filter((g) => g.no > hedef)) {
    if (!uygulanan(db).includes(goc.no)) continue;
    for (const komut of goc.geri) { db.exec(komut); db.sorgu += 1; }
    db.prepare('DELETE FROM goc_kaydi WHERE no = ?').run(goc.no);
    db.sorgu += 1;
  }
  return db;
}

export const sutunlar = (db, tablo) =>
  db.prepare(`PRAGMA table_info(${tablo})`).all().map((s) => `${s.name}:${s.notnull}`);
```

#### Dört Test

Birinci test şemanın **şeklini** doğrular: hangi sütunlar, hangi sırada ve hangisi boş
geçilemez. Beklenen liste elle yazılır; göç bir sütun eklediğinde ya da unuttuğunda bu
liste ayrışır. İkinci test göç numaralarının tekil ve boşluksuz olduğunu sorar; yakaladığı
sınıf, iki ayrı dalda aynı numarayla açılan iki göçtür. Üçüncü test gidiş-dönüş yapar.
Dördüncü test göçü var olan veri üzerinde çalıştırır.

```js
// goc.test.mjs — ileri goc, goc numarasi, gidis-donus ve kisit ihlali testleri
import test from 'node:test';
import assert from 'node:assert/strict';
import { GOCLER } from './gocler.mjs';
import { veritabaniAc, ileri, geri, uygulanan, sutunlar, sirayiDenetle } from './kosucu.mjs';

const uyeEkle = (db, no, ad, sube) =>
  db.prepare('INSERT INTO uye (uye_no, ad, sube) VALUES (?, ?, ?)').run(no, ad, sube);

test('ileri goc semayi beklenen bicime getirir', () => {
  const db = ileri(veritabaniAc(), 4);
  assert.deepEqual(uygulanan(db), [1, 2, 3, 4]);
  assert.deepEqual(sutunlar(db, 'uye'), ['uye_no:0', 'ad:1', 'sube:1', 'soyad:0']);
  assert.deepEqual(sutunlar(db, 'odunc'),
    ['kitap_no:0', 'uye_no:1', 'alis_gunu:1', 'son_gun:1', 'sube:0']);
});

test('goc numaralari tekil ve bosluksuz olmali', () => {
  assert.equal(sirayiDenetle(GOCLER), 4);
  const catisan = [...GOCLER, { no: 4, ad: 'baska dalda acilan goc', ileri: [], geri: [] }];
  assert.throws(() => sirayiDenetle(catisan), /goc numarasi yineleniyor: 4/);
});

test('gidis-donus uye adini korur', () => {
  const db = ileri(veritabaniAc(), 2);
  uyeEkle(db, 'U-0001', 'Ayse Demir', 'merkez');
  ileri(db, 3);
  geri(db, 2);
  assert.equal(db.prepare('SELECT ad FROM uye WHERE uye_no = ?').get('U-0001').ad, 'Ayse Demir');
});

test('dorduncu goc yinelenen ad uzerinde kisit ihlaliyle duser', () => {
  const db = ileri(veritabaniAc(), 2);
  uyeEkle(db, 'U-0001', 'Ayse Demir', 'merkez');
  uyeEkle(db, 'U-0002', 'Ayse Demir', 'kampus');
  ileri(db, 3);
  assert.throws(() => ileri(db, 4), /UNIQUE constraint failed/);
});
```

```sh
GERI_GOC=kusurlu node --test --test-reporter=tap goc.test.mjs | grep -E '^ *(ok|not ok|(expected|actual):|# (tests|pass|fail))'
```

```
ok 1 - ileri goc semayi beklenen bicime getirir
ok 2 - goc numaralari tekil ve bosluksuz olmali
not ok 3 - gidis-donus uye adini korur
  expected: 'Ayse Demir'
  actual: 'Ayse'
ok 4 - dorduncu goc yinelenen ad uzerinde kisit ihlaliyle duser
# tests 4
# pass 3
# fail 1
```

Yakalanan kusur sınıfının adı **geri göçte veri kaybıdır**. Üçüncü göçün ileri yönü adı
ikiye bölüyor, geri yönü ise ikinci parçayı taşıyan sütunu doğrudan düşürüyor; soyadı
tutan tek yer o sütun olduğu için geri dönüş adı yarım bırakıyor. Birinci testin yeşil
kalması bu sınıfın neden kaçtığını da söylüyor: sütun listesi geri göçten sonra doğru
görünüyor, taşıdığı değer yanlış.

Düzeltme, sütunu düşürmeden önce içeriğini geri birleştirmektir. Göç kümesinde bu sürüm
zaten yazılı.

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

```
ok 1 - ileri goc semayi beklenen bicime getirir
ok 2 - goc numaralari tekil ve bosluksuz olmali
ok 3 - gidis-donus uye adini korur
ok 4 - dorduncu goc yinelenen ad uzerinde kisit ihlaliyle duser
# tests 4
# pass 4
# fail 0
```

Dördüncü testin her iki koşumda da geçmesi ayrı bir bilgidir. Teklik kısıtı ekleyen göç,
var olan veride yinelenen bir çift bulduğunda düşer; test bunu bir kusur olarak değil,
**belgelenmiş davranış** olarak sabitliyor. Böyle bir göçün üretimde uygulanabilmesi için
önünde bir temizleme adımı bulunması gerektiği buradan okunur.

#### Şema Testinin Görmediği

Birinci test şemanın şeklini doğruluyor: `odunc` tablosuna `sube` sütunu eklendi, adı ve
sırası doğru. İkinci göçün o sütuna **ne yazdığı** hiçbir testte sorulmuyor.

```js
// geri-doldurma.mjs — sema testi yesilken ikinci gocun yazdigi degerin ne oldugu
import { veritabaniAc, ileri, sutunlar } from './kosucu.mjs';

const db = ileri(veritabaniAc(), 1);
const uye = db.prepare('INSERT INTO uye (uye_no, ad, sube) VALUES (?, ?, ?)');
const odunc = db.prepare('INSERT INTO odunc (kitap_no, uye_no, alis_gunu, son_gun) VALUES (?, ?, 900, 914)');
for (const [no, ad, sube] of [['U-1', 'Ayse Demir', 'merkez'], ['U-2', 'Emre Koc', 'kampus'], ['U-3', 'Elif Sahin', 'sanat']]) {
  uye.run(no, ad, sube);
  odunc.run(`K-${no}`, no);
}
ileri(db, 2);

const dagilim = (tablo) => db.prepare(`SELECT sube, COUNT(*) AS n FROM ${tablo} GROUP BY sube ORDER BY sube`)
  .all().map((s) => `${s.sube}=${s.n}`).join(' ');

console.log(`sema testinin gordugu : odunc sutunlari ${sutunlar(db, 'odunc').join(' ')}`);
console.log(`uye tablosundaki sube : ${dagilim('uye')}`);
console.log(`goc sonrasi odunc sube: ${dagilim('odunc')}`);
```

```
sema testinin gordugu : odunc sutunlari kitap_no:0 uye_no:1 alis_gunu:1 son_gun:1 sube:0
uye tablosundaki sube : kampus=1 merkez=1 sanat=1
goc sonrasi odunc sube: merkez=3
```

Üyeler üç ayrı şubede, ödünçlerin tamamı merkezde. İkinci göç sütunu sabit bir değerle
doldurdu; üyenin şubesinden okumadı. Kaçan kusur sınıfının adı **geri doldurma değeri
uyuşmazlığıdır** ve şema testi onu göremez, çünkü şema testinin kâhini sütun listesidir,
satır içerikleri değil. Bu sınıfın yakalanması göç öncesi ve sonrası veri üzerinde
değişmez ifadeler kurmayı gerektirir — "her ödüncün şubesi, üyesinin şubesine eşittir"
gibi — ve o iddia bir önceki dersin veri kümeleriyle sınanır.

#### Bedel

```js
// bedel.mjs — goc testinin bedeli: adim, sorgu, dosya ve goreli sure
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { GOCLER } from './gocler.mjs';
import { veritabaniAc, ileri, geri } from './kosucu.mjs';

const kok = mkdtempSync(join(tmpdir(), 'goc-'));
const adim = GOCLER.reduce((t, g) => t + g.ileri.length + g.geri.length, 0);

const sifirdan = performance.now();
const db = ileri(veritabaniAc(join(kok, 'katalog.db')), 4);
const sifirdanSure = performance.now() - sifirdan;
const ileriSorgu = db.sorgu;

const gidisDonus = performance.now();
geri(db, 2);
ileri(db, 4);
const gidisDonusSure = performance.now() - gidisDonus;

const boyut = statSync(join(kok, 'katalog.db')).size;
rmSync(kok, { recursive: true, force: true });

console.log(`goc adimi (ileri + geri komut) : ${adim}`);
console.log(`sifirdan dorde cikan sorgu     : ${ileriSorgu}`);
console.log(`gidis-donus sonrasi toplam     : ${db.sorgu}`);
console.log(`olusan dosya                   : 1, ${boyut} bayt`);
console.log(`gidis-donus sifirdan gocten uzun surmedi : ${gidisDonusSure < sifirdanSure * 3}`);
```

```sh
GERI_GOC=duzeltilmis node bedel.mjs
```

```
goc adimi (ileri + geri komut) : 13
sifirdan dorde cikan sorgu     : 11
gidis-donus sonrasi toplam     : 21
olusan dosya                   : 1, 28672 bayt
gidis-donus sifirdan gocten uzun surmedi : true
```

On üç göç komutu, sıfırdan dörde çıkan on bir sorgu, gidiş-dönüşle birlikte yirmi bir
sorgu ve bir dosya. Bu sayılar göç kümesi büyüdükçe doğrusal artar: her yeni adım hem ileri
hem geri komutunu, hem de birinci testin beklenen sütun listesinde bir satır değişikliğini
getirir. Bakım bedeli buradadır — bir sütun eklemek üç dosyaya dokunmayı gerektirir: göç
kümesi, beklenen şema listesi ve o sütunu okuyan sorgular.

#### Özet

- Şema testinin kâhini elle yazılan sütun listesidir; göç bir sütunu eklemeyi ya da
  düşürmeyi unuttuğunda bu liste ayrışır.
- Göç numaralarının tekilliği ayrı bir testtir ve iki dalda aynı numarayla açılan göçleri
  yakalar.
- Gidiş-dönüş testi geri göçte veri kaybını yakaladı: sütunu düşürmeden önce içeriğini
  birleştirmeyen geri göç, adı yarım bıraktı.
- Teklik kısıtı ekleyen göç, var olan veride yinelenen bir çift bulduğunda düşer; test bunu
  belgelenmiş davranış olarak sabitler.
- Şema testi geri doldurma değerini göremez: sütun eklendi, tüm satırlara sabit bir değer
  yazıldı ve test yeşil kaldı.

#### Sonraki Adım

Bu derste gerçek çalıştırılan bağımlılık aynı makinede duruyordu; şeması elde, verisi
üretilebilir, göçü tekrarlanabilirdi. Bütün bağımlılıklar böyle değildir. Kütüphanenin
ödünç akışı, kimlik doğrulaması yapan ve kitap künyesi döndüren dış bir servise bağlıdır:
o servis başka bir ekibindir, testin komutuyla ayağa kalkmaz, her koşumda aynı yanıtı
vermeyebilir ve bazı koşumlarda hiç yanıt vermez. Sonraki ders bu bağımlılığı kayıt ve
tekrar oynatma ile ele alır: gerçek yanıtlar bir kez kaydedilir, sonraki koşumlar kayıttan
beslenir ve bunun hangi kusur sınıfını yakaladığı, hangisini kaçırdığı ölçülür.

### Dış Servis Taklidi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/dis-servis-taklidi

Başka bir ekibin servisinin kayıt ve tekrar oynatma ile taklit edilmesi: kayıttan beslenen koşumun süreç ve ağ çağrısı olmadan çalışması, istek biçimi kaymasının kasette karşılık bulamayarak yakalanması ve kaydın dondurduğu yanıtın sözleşme değiştiğinde yeşil kalması.

Önceki derste gerçek çalıştırılan bağımlılık aynı makinede duruyordu: şeması elde, göçü
tekrarlanabilirdi. Kütüphanenin ödünç akışı bir de künye servisine bağlıdır ve o servis
başka bir ekibindir. Testin komutuyla ayağa kalkmaz, her koşumda aynı yanıtı vermeyebilir
ve bazı koşumlarda hiç yanıt vermez.

Bu bağımlılık için kullanılan yaklaşımın adı **kayıt ve tekrar oynatmadır** (record and
replay): gerçek yanıtlar bir kez kaydedilir, sonraki koşumlar kayıttan beslenir. Kayıt
dosyasına **kaset** denir. Bu ders kasetin yakaladığı ve kaçırdığı kusur sınıflarını
ölçer.

#### Dış Servis ve Taşıyıcı

Künye servisi burada yerel bir süreçle temsil ediliyor; gerçekte ağın öbür ucundadır. İki
sürümü vardır ve aradaki fark bir alan adıdır — bu fark dersin sonunda ölçülecek.

```js
// kunye-servisi.mjs — baska bir ekibin kunye servisinin yerine gecen yerel surec
import { createServer } from 'node:http';

const KUNYELER = {
  'K-903': { yazar: 'Sabahattin Ali', yil: 1943 },
  'K-904': { yazar: 'Halide Edip', yil: 1922 },
};

export function servisKur(bagl, surum) {
  const sunucu = createServer((istek, yanit) => {
    const adres = new URL(istek.url, 'http://yerel');
    const no = adres.searchParams.get('no');
    const kunye = KUNYELER[no];
    if (adres.pathname !== '/kunye' || kunye === undefined) {
      yanit.writeHead(404, { 'content-type': 'application/json' });
      return yanit.end(`{"hata":"kunye yok: ${no}"}`);
    }
    // v2, baslik alanini ad olarak yeniden adlandirdi
    const govde = surum === 'v2'
      ? { no, ad: `kitap ${no}`, yazar: kunye.yazar, yil: kunye.yil }
      : { no, baslik: `kitap ${no}`, yazar: kunye.yazar, yil: kunye.yil };
    yanit.writeHead(200, { 'content-type': 'application/json' });
    yanit.end(JSON.stringify(govde));
  });
  sunucu.listen(bagl, '127.0.0.1');
  return sunucu;
}

if (process.argv[2] !== undefined) servisKur(Number(process.argv[2]), process.argv[3] ?? 'v1');
```

Taşıyıcı iki kipte çalışır. Kayıt kipinde gerçek servise gider ve her etkileşimi kasete
ekler; oynatma kipinde ağa hiç çıkmaz, isteği kasette arar. Anahtar, yöntem ile tam yoldan
oluşur — sorgu alanları dahil.

```js
// kaset.mjs — kayit ve tekrar oynatma: kayit kipi gercek servise gider, oynatma kipi dosyadan okur
import { existsSync, readFileSync, writeFileSync } from 'node:fs';

export function kasetIstemcisi({ kip, kasetYolu, taban }) {
  const kaset = existsSync(kasetYolu) ? JSON.parse(readFileSync(kasetYolu, 'utf8')) : [];
  let agCagrisi = 0;
  return {
    async iste(yol) {
      const anahtar = `GET ${yol}`;
      if (kip === 'oynat') {
        const kayit = kaset.find((k) => k.anahtar === anahtar);
        if (kayit === undefined) throw new Error(`kasette karsilik yok: ${anahtar}`);
        return { durum: kayit.durum, govde: kayit.govde };
      }
      const yanit = await fetch(`${taban}${yol}`);
      agCagrisi += 1;
      const govde = await yanit.json();
      kaset.push({ anahtar, durum: yanit.status, govde });
      writeFileSync(kasetYolu, `${JSON.stringify(kaset, null, 1)}\n`);
      return { durum: yanit.status, govde };
    },
    olcu: () => ({ agCagrisi, etkilesim: kaset.length }),
  };
}
```

```js
// hazir.mjs — servisin dinlemeye baslamasini bekler
export async function hazirBekle(taban, enCok = 100) {
  for (let i = 0; i < enCok; i += 1) {
    try { await (await fetch(`${taban}/kunye?no=K-903`)).text(); return; }
    catch { await new Promise((c) => setTimeout(c, 20)); }
  }
  throw new Error('servis hazir olmadi');
}
```

Künye istemcisi sorgu alanının adını bir değişkenden alır; kusurlu sürüm bu adı
değiştirecek.

```js
// istemci.mjs — kunye istemcisi; ALAN_ADI degiskeni sorgu alaninin adini secer
export const SORGU_ALANI = process.env.ALAN_ADI ?? 'no';

export function kunyeIstemcisi(tasiyici) {
  return {
    async kunyeGetir(kitapNo) {
      const { durum, govde } = await tasiyici.iste(`/kunye?${SORGU_ALANI}=${kitapNo}`);
      if (durum !== 200) throw new Error(`kunye alinamadi: ${durum}`);
      return govde;
    },
  };
}
```

```js
// kunye.test.mjs — ayni test uc kipte: kayit, tekrar oynatma ve canli servis
import test, { before } from 'node:test';
import assert from 'node:assert/strict';
import { kasetIstemcisi } from './kaset.mjs';
import { kunyeIstemcisi } from './istemci.mjs';
import { hazirBekle } from './hazir.mjs';

const kip = process.env.KIP ?? 'oynat';
const kasetYolu = process.env.KASET ?? 'kaset.json';
const taban = `http://127.0.0.1:${process.env.BAGL ?? 8941}`;
const istemciKur = () => kunyeIstemcisi(kasetIstemcisi({ kip, kasetYolu, taban }));

before(async () => { if (kip === 'kayit') await hazirBekle(taban); });

test('K-903 kunyesinde baslik ve yazar bulunur', async () => {
  const kunye = await istemciKur().kunyeGetir('K-903');
  assert.equal(kunye.baslik, 'kitap K-903');
  assert.equal(kunye.yazar, 'Sabahattin Ali');
});

test('K-904 kunyesinde yil bulunur', async () => {
  const kunye = await istemciKur().kunyeGetir('K-904');
  assert.equal(kunye.yil, 1922);
});
```

#### Kayıt ve Oynatma

Kayıt bir kez yapılır ve gerçek servisi gerektirir.

```sh
node kunye-servisi.mjs 8941 v1 & SERVIS=$!
KIP=kayit node --test --test-reporter=tap kunye.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
kill $SERVIS
echo "kasetteki etkilesim: $(node -e "console.log(JSON.parse(require('fs').readFileSync('kaset.json')).length)")"
```

```
ok 1 - K-903 kunyesinde baslik ve yazar bulunur
ok 2 - K-904 kunyesinde yil bulunur
# tests 2
# pass 2
# fail 0
kasetteki etkilesim: 2
```

Sonraki koşumlar servisi hiç istemez.

```sh
node --test --test-reporter=tap kunye.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
echo "kosum sirasinda ayakta olan kunye servisi: $(pgrep -f 'kunye-servisi.mjs' | wc -l | tr -d ' ')"
```

```
ok 1 - K-903 kunyesinde baslik ve yazar bulunur
ok 2 - K-904 kunyesinde yil bulunur
# tests 2
# pass 2
# fail 0
kosum sirasinda ayakta olan kunye servisi: 0
```

İkinci ve üçüncü derste ödenen bedel burada ödenmiyor: ayağa kalkan süreç yok, hazır olma
yoklaması yok, yıkım adımı yok. Karşılığında kasete bir bağımlılık doğdu ve kasetin
geçerliliği bir varsayımdır.

#### Yakalanan Sınıf: İstek Biçimi Kayması

Künye istemcisinin sorgu alanı `no` yerine `kitap` olarak değiştirilsin. Gerçek servis
buna 404 döndürürdü; kaset karşılık bulamıyor.

```sh
ALAN_ADI=kitap node --test --test-reporter=tap kunye.test.mjs \
  | grep -E "^ *(not ok|error:|# fail)" | sed 's/^ *//;s/^/kusurlu     /'
ALAN_ADI=no node --test --test-reporter=tap kunye.test.mjs \
  | grep -E '^ *(ok|# (pass|fail))' | sed 's/^ *//;s/^/duzeltilmis /'
```

```
kusurlu     not ok 1 - K-903 kunyesinde baslik ve yazar bulunur
kusurlu     error: 'kasette karsilik yok: GET /kunye?kitap=K-903'
kusurlu     not ok 2 - K-904 kunyesinde yil bulunur
kusurlu     error: 'kasette karsilik yok: GET /kunye?kitap=K-904'
kusurlu     # fail 2
duzeltilmis ok 1 - K-903 kunyesinde baslik ve yazar bulunur
duzeltilmis ok 2 - K-904 kunyesinde yil bulunur
duzeltilmis # pass 2
duzeltilmis # fail 0
```

Yakalanan kusur sınıfının adı **istek biçimi kaymasıdır**. Kaset yalnız yanıtları değil,
o yanıtları getiren **istekleri** de sabitliyor; anahtarın içinde yol ve sorgu alanları
bulunduğu için istek tarafındaki her değişiklik karşılıksız kalıyor. Bu, kasetin en güçlü
yanıdır ve aynı zamanda en çok gürültü üreten yanıdır: kodun dış servise yaptığı çağrı
gerçekten değiştiğinde de aynı hata çıkar ve kaset yeniden kaydedilmek zorunda kalır —
yeniden kayıt ise gerçek servisi yeniden gerektirir.

#### Kaçan Sınıf: Dondurulmuş Yanıt

Kaset, servisin **kayıt anındaki** davranışını dondurur. Servis sahibi ekip yanıt gövdesini
değiştirdiğinde kasetin bundan haberi olmaz.

```sh
node --test --test-reporter=tap kunye.test.mjs | grep -E '^ *(ok|not ok|# (pass|fail))' | sed 's/^/oynatma  /'
node kunye-servisi.mjs 8942 v2 & SERVIS=$!
KIP=kayit KASET=kaset-v2.json BAGL=8942 node --test --test-reporter=tap kunye.test.mjs \
  | grep -E '^ *(ok|not ok|# (pass|fail))' | sed 's/^/canli v2 /'
kill $SERVIS
rm -f kaset-v2.json
```

```
oynatma  ok 1 - K-903 kunyesinde baslik ve yazar bulunur
oynatma  ok 2 - K-904 kunyesinde yil bulunur
oynatma  # pass 2
oynatma  # fail 0
canli v2 not ok 1 - K-903 kunyesinde baslik ve yazar bulunur
canli v2 ok 2 - K-904 kunyesinde yil bulunur
canli v2 # pass 1
canli v2 # fail 1
```

Aynı test, aynı kod, iki farklı yanıt. Servisin ikinci sürümü `baslik` alanını `ad` olarak
yeniden adlandırdı; canlı koşum bunu görüyor, kasetten beslenen koşum görmüyor. Kaçan kusur
sınıfının adı **dondurulmuş yanıttır**: kaset dünkü davranışı taşıyor ve o davranış artık
doğru değil.

Bu sınıfın ağırlığı, sessiz olmasından gelir. Şema uyuşmazlığı bir kısıt ihlaliyle, göç
kusuru bir iddia düşmesiyle, istek kayması bir eksik karşılıkla kendini duyurdu.
Dondurulmuş yanıtta düşen hiçbir şey yoktur — takım yeşildir ve yeşil olduğu için kimse
bakmaz.

#### Bedel

```js
// bedel.mjs — kayit ve oynatma kiplerinin bedeli: surec, ag cagrisi, dosya ve goreli sure
import { spawn } from 'node:child_process';
import { statSync, rmSync } from 'node:fs';
import { kasetIstemcisi } from './kaset.mjs';
import { kunyeIstemcisi } from './istemci.mjs';
import { hazirBekle } from './hazir.mjs';

const N = 100;
const taban = 'http://127.0.0.1:8943';
const surec = spawn(process.execPath, ['kunye-servisi.mjs', '8943', 'v1'], { stdio: 'ignore' });
await hazirBekle(taban);

async function olc(kip, kasetYolu) {
  const tasiyici = kasetIstemcisi({ kip, kasetYolu, taban });
  const istemci = kunyeIstemcisi(tasiyici);
  await istemci.kunyeGetir('K-903');
  const baslangic = performance.now();
  for (let i = 0; i < N; i += 1) await istemci.kunyeGetir('K-903');
  return { sure: performance.now() - baslangic, ...tasiyici.olcu() };
}

const canli = await olc('kayit', 'kaset-olcum.json');
const oynatma = await olc('oynat', 'kaset.json');
surec.kill('SIGTERM');
const boyut = statSync('kaset.json').size;
rmSync('kaset-olcum.json', { force: true });

console.log(`kayit kipi   : 1 surec, ${canli.agCagrisi} ag cagrisi (${N} istek + 1 isinma)`);
console.log(`oynatma kipi : 0 surec, ${oynatma.agCagrisi} ag cagrisi, 1 dosya okumasi`);
console.log(`kaset        : ${oynatma.etkilesim} etkilesim, ${boyut} bayt`);
console.log(`oynatma daha kisa surdu       : ${oynatma.sure < canli.sure}`);
console.log(`sure farki en az yirmi kat    : ${canli.sure / oynatma.sure >= 20}`);
```

```
kayit kipi   : 1 surec, 101 ag cagrisi (100 istek + 1 isinma)
oynatma kipi : 0 surec, 0 ag cagrisi, 1 dosya okumasi
kaset        : 2 etkilesim, 334 bayt
oynatma daha kisa surdu       : true
sure farki en az yirmi kat    : true
```

Oynatma kipinin koşum bedeli üç dersin en düşüğüdür: sıfır süreç, sıfır ağ çağrısı, tek
dosya okuması. Bedel koşumdan çıkıp başka bir yere taşınmıştır — üç yüz otuz dört baytlık
kasetin **tazeliğine**. Bu bedel her koşumda değil, servis değiştiğinde ödenir ve ödenmediği
zaman görünmez. Kaset büyüdükçe de büyür: iki etkileşim gözle denetlenebilir, iki yüz
etkileşim denetlenemez.

#### Özet

- Kayıt ve tekrar oynatma, gerçek yanıtları bir kez kaydedip sonraki koşumları kasetten
  besler; oynatma kipinde süreç ve ağ çağrısı sıfırdır.
- Kaset yalnız yanıtları değil istekleri de sabitler; sorgu alanının adı değiştiğinde iki
  test de kasette karşılık bulamadı.
- Kaydın dondurduğu yanıt eskiyebilir: servisin ikinci sürümü bir alanı yeniden
  adlandırdığında canlı koşum kırmızıya döndü, kasetten beslenen koşum yeşil kaldı.
- Dondurulmuş yanıt sessiz bir kusur sınıfıdır; düşen bir test olmadığı için bakılmaz.
- Bedel koşumdan kasetin tazeliğine taşınır ve kaset büyüdükçe elle denetlenemez hâle
  gelir.

#### Sonraki Adım

Bu konudaki beş ders aynı soruyu farklı sınırlarda sordu: hangi bağımlılık gerçek çalışır
ve bunun karşılığı nedir. Kaset, sorunun sınırına geldi. Kayıt, dış servisin **dünkü**
davranışını donduruyor; servis sahibi ekip sözleşmesini değiştirdiğinde kayıt hâlâ yeşil
kalıyor ve testler yalan söylemeye başlıyor. Kaseti tazelemek bir çözüm değildir, çünkü
tazelemenin ne zaman gerektiğini söyleyen hiçbir sinyal yoktur.

Eksik olan şey, iki tarafın üzerinde anlaştığı **sözleşmenin kendisinin** sınanmasıdır:
tüketicinin beklediği alanlar sağlayıcının verdiği alanlarla örtüşüyor mu, bir alan adı
değiştiğinde bunu kim ve ne zaman fark eder. Sonraki konu bu soruyla açılır ve isteğin ve
yanıtın doğrulanmasını, beklentinin sağlayıcıya taşınmasını ve kırıcı değişikliğin denetimini
ele alır.

## Sözleşme ve API Testleri

İki tarafın aynı sözleşmeye uyduğunun tek tek sınanması: istek ve yanıtın şema denetimiyle doğrulanması, tüketicinin beklentisinin sağlayıcıya taşınması, kırıcı değişikliğin yayımdan önce yakalanması ve sahte sunucuların paralel geliştirmeyi mümkün kılarken taşıdığı sapma riski.

### API Testi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/api-testi

Canlı bir servisin yanıtının şemaya karşı denetlenmesi: donmuş kaydın göremediği alan değişikliğinin gerçek istekle yakalanması, yapısal uyuşmazlık ile anlamsal uyuşmazlığın ayrılması ve denetimin bedelinin denetlenen alan, giden istek ve süre payıyla ölçülmesi.

Önceki ders dış servisin yanıtını kaydedip tekrar oynatmayı kurdu ve bir uyarıyla bitti:
kayıt, servisin **dünkü** davranışını dondurur. Katalog servisi bugün bir alanın adını
değiştirdiğinde kayda karşı koşan test hâlâ yeşil kalır, çünkü karşılaştırdığı şey servisin
kendisi değil, dünkü kopyasıdır.

Bu ders o boşluğu kapatan denetimi kurar. Sorusu API tasarımı değildir; istek yöntemleri,
durum kodları ve sürümleme Web API Tasarımı kursunda kuruldu ve burada tekrarlanmaz.
Buradaki soru şudur: canlı bir servisin yanıtı tüketicinin beklediği biçimde mi geliyor,
bunu hangi denetim söylüyor, hangi kusur sınıfını görerek ve hangisini göremeyerek.

#### Donmuş Kaydın Sessizliği

Kütüphane ödünç sisteminde iki servis var: kitap kayıtlarını tutan **katalog servisi** ve
üyeye ödünç veren **ödünç servisi**. Ödünç servisi katalogdan bir kitap kaydı okur ve dört
alanına bakar: durum, raftaki adet, ödünç süresi ve kimlik. Aşağıdaki betik dün kaydedilen
gövdeyi ve bugünkü gövdeyi aynı karar işlevine verir.

```js
// donmus.mjs — dun kaydedilen katalog yaniti ile bugunku yanit, ayni tuketici islevinde
const KAYIT = {
  isbn: "978-0262033848", baslik: "Algoritmalara Giris", yazar: "Cormen",
  basimYili: 2009, sube: "merkez", toplamAdet: 4, raftaAdet: 2,
  durum: "rafta", oduncGun: 14,
};

// Katalog servisi bugun alan adini degistirdi: oduncGun -> oduncSuresiGun.
const CANLI = { ...KAYIT, oduncSuresiGun: KAYIT.oduncGun };
delete CANLI.oduncGun;

// Odunc servisinin karar islevi: kayitta ne varsa onu okur.
const oduncKarari = (kitap, bugun) =>
  kitap.durum !== "rafta" || kitap.raftaAdet < 1
    ? { verilir: false, sonGun: null }
    : { verilir: true, sonGun: bugun + kitap.oduncGun };

const BUGUN = 20260, ortak = Object.keys(KAYIT).filter((a) => a in CANLI);
for (const [ad, govde] of [["kayit", KAYIT], ["canli", CANLI]]) {
  const karar = oduncKarari(govde, BUGUN);
  console.log(`${ad.padEnd(6)} alan ${String(Object.keys(govde).length).padStart(2)}` +
    `  verilir=${karar.verilir}  sonGun=${karar.sonGun}`);
}
const eksik = Object.keys(KAYIT).filter((a) => !(a in CANLI));
console.log(`ortusen alan ${ortak.length}/${Object.keys(KAYIT).length}; canli yanitta olmayan: ${eksik.join(", ")}`);
console.log(`tekrar oynatma testi kayda karsi kosar -> sonGun 20274 bekleniyor, yesil`);
```

```
kayit  alan  9  verilir=true  sonGun=20274
canli  alan  9  verilir=true  sonGun=NaN
ortusen alan 8/9; canli yanitta olmayan: oduncGun
tekrar oynatma testi kayda karsi kosar -> sonGun 20274 bekleniyor, yesil
```

İki gövde de dokuz alan taşıyor ve sekizi örtüşüyor. Ödünç servisi ise canlı yanıtla
çalıştığında son günü hesaplayamıyor. Kayıt ve tekrar oynatma düzeni bunu göremez, çünkü
canlı servise **hiç istek gitmez**; yakalanan hata sınıfı sıfırdır. Eksik olan denetim,
yanıtın gerçekten geldiği yerden alınıp yazılı bir beklentiye karşı sınanmasıdır.

#### Yanıtın Şeması

**Şema** yanıtın hangi alanları hangi türde taşıdığını yazılı hâle getirir. Aşağıdaki tanım
dokuz alan ve on altı kural içeriyor; kurallar tür, desen, değer kümesi ve alt sınırdan
ibarettir. Denetleyici de birkaç satırdır ve bir uyuşmazlık listesi döndürür.

```js
// sema.mjs — katalog yanitinin semasi ve alan alan denetleyici
export const KITAP_SEMA = {
  isbn: { tip: "dizgi", desen: /^97[89]-\d{10}$/ },
  baslik: { tip: "dizgi" },
  yazar: { tip: "dizgi" },
  basimYili: { tip: "tamsayi", enAz: 1400 },
  sube: { tip: "dizgi", kume: ["merkez", "sahil", "tepe"] },
  toplamAdet: { tip: "tamsayi", enAz: 0 },
  raftaAdet: { tip: "tamsayi", enAz: 0 },
  durum: { tip: "dizgi", kume: ["rafta", "oduncte", "kayip"] },
  oduncGun: { tip: "tamsayi", enAz: 1 },
};

const tipiTut = (kural, d) => kural.tip === "tamsayi"
  ? Number.isInteger(d)
  : typeof d === "string";

// Donen dizi bostur ya da her satiri bir uyusmazliktir.
export function semayaGore(sema, govde) {
  const uyusmazlik = [];
  for (const [ad, kural] of Object.entries(sema)) {
    const d = govde[ad];
    if (d === undefined) { uyusmazlik.push(`${ad}: alan yok`); continue; }
    if (!tipiTut(kural, d)) { uyusmazlik.push(`${ad}: tip ${kural.tip} degil (${typeof d})`); continue; }
    if (kural.desen && !kural.desen.test(d)) uyusmazlik.push(`${ad}: desene uymuyor`);
    if (kural.kume && !kural.kume.includes(d)) uyusmazlik.push(`${ad}: kume disi deger ${d}`);
    if (kural.enAz !== undefined && d < kural.enAz) uyusmazlik.push(`${ad}: ${kural.enAz} altinda`);
  }
  const fazla = Object.keys(govde).filter((a) => !(a in sema));
  if (fazla.length) uyusmazlik.push(`semada olmayan alan: ${fazla.join(", ")}`);
  return uyusmazlik;
}
```

Şemanın söyledikleri ile söylemedikleri arasındaki sınır bu dersin ekseni. Şema `raftaAdet`
alanının sıfırdan küçük olamayacağını söyler; o sayının **doğru** sayı olduğunu söylemez.

#### Canlı Servise Karşı Denetim

Katalog servisi yerel bir süreçtir ve bir çevre değişkeniyle bilerek bozulabilir. Üç kusur
tanımlı: `tip` raf adedini dizgi olarak döndürür, `anlam` raf adedi yerine toplam adedi
yazar, `deger` başvuru kitabının yedi günlük ödünç süresini on dörde çıkarır.

```js
// katalog.mjs — katalog servisi; KATALOG_KUSUR degiskeniyle bilerek bozulur
// Kullanim: node katalog.mjs <port|0>   port 0 verilirse bos bir port secilir.
import { createServer } from "node:http";

if (process.argv[2] === undefined) {
  console.log("kullanim: node katalog.mjs <port|0>");
  process.exit(0);
}

const KUSUR = process.env.KATALOG_KUSUR ?? "yok";
const KITAPLAR = {
  "978-0262033848": { isbn: "978-0262033848", baslik: "Algoritmalara Giris", yazar: "Cormen",
    basimYili: 2009, sube: "merkez", toplamAdet: 4, raftaAdet: 2, durum: "rafta", oduncGun: 14 },
  "978-0201896831": { isbn: "978-0201896831", baslik: "Basvuru El Kitabi", yazar: "Knuth",
    basimYili: 1997, sube: "tepe", toplamAdet: 3, raftaAdet: 0, durum: "oduncte", oduncGun: 7 },
};

const disaVer = (k) => {
  if (KUSUR === "tip") return { ...k, raftaAdet: String(k.raftaAdet) };
  if (KUSUR === "anlam") return { ...k, raftaAdet: k.toplamAdet };
  if (KUSUR === "deger") return { ...k, oduncGun: 14 };
  return { ...k };
};

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;
  const kitap = KITAPLAR[istek.url.split("?")[0].replace("/kitap/", "")];
  yanit.writeHead(kitap ? 200 : 404, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(kitap ? disaVer(kitap) : { hata: "kitap yok" }));
});

sunucu.listen(Number(process.argv[2]), "127.0.0.1", () =>
  console.log(`hazir ${sunucu.address().port} kusur=${KUSUR}`));
```

Test dosyası servisi ayrı bir süreç olarak başlatır, seçilen portu sürecin ilk satırından
okur ve üç şey sınar: şemaya uygunluk, bir **değişmez** (invariant) ve tüketicinin kararı.

```js
// api.test.mjs — canli katalog servisine karsi istek/yanit denetimi
// Kullanim: KATALOG_KUSUR=yok|tip|anlam|deger node --test api.test.mjs
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { KITAP_SEMA, semayaGore } from "./sema.mjs";

let surec, taban;

before(() => new Promise((coz) => {
  surec = spawn("node", ["katalog.mjs", "0"], { env: process.env });
  surec.stdout.once("data", (v) => {
    taban = `http://127.0.0.1:${String(v).split(" ")[1]}`;
    coz();
  });
}));
after(() => surec.kill());

const kitap = async (isbn) => (await fetch(`${taban}/kitap/${isbn}`)).json();

test("sema: iki kitap yaniti da semaya uyar", async () => {
  for (const isbn of ["978-0262033848", "978-0201896831"]) {
    const uyusmazlik = semayaGore(KITAP_SEMA, await kitap(isbn));
    assert.equal(uyusmazlik.length, 0, `${isbn} -> ${uyusmazlik.join("; ")}`);
  }
});

test("degismez: oduncte olan kitabin rafta adedi sifirdir", async () => {
  const k = await kitap("978-0201896831");
  assert.equal(k.durum === "oduncte" && k.raftaAdet > 0, false,
    `${k.isbn} -> durum ${k.durum} ama raftaAdet ${k.raftaAdet}`);
});

test("tuketici: rafta olan kitap icin son gun hesaplanir", async () => {
  const k = await kitap("978-0262033848");
  assert.equal(k.durum, "rafta");
  assert.equal(Number.isInteger(20260 + k.oduncGun), true);
});
```

Aynı takım servisin dört durumuna karşı koşturulur: sağlam sürüm ve üç kusur.

```bash
for k in yok tip anlam deger; do
  echo "== kusur=$k =="
  KATALOG_KUSUR=$k node --test --test-reporter=tap api.test.mjs | grep -E '^(ok|not ok|# (pass|fail))'
done
```

```
== kusur=yok ==
ok 1 - sema: iki kitap yaniti da semaya uyar
ok 2 - degismez: oduncte olan kitabin rafta adedi sifirdir
ok 3 - tuketici: rafta olan kitap icin son gun hesaplanir
# pass 3
# fail 0
== kusur=tip ==
not ok 1 - sema: iki kitap yaniti da semaya uyar
ok 2 - degismez: oduncte olan kitabin rafta adedi sifirdir
ok 3 - tuketici: rafta olan kitap icin son gun hesaplanir
# pass 2
# fail 1
== kusur=anlam ==
ok 1 - sema: iki kitap yaniti da semaya uyar
not ok 2 - degismez: oduncte olan kitabin rafta adedi sifirdir
ok 3 - tuketici: rafta olan kitap icin son gun hesaplanir
# pass 2
# fail 1
== kusur=deger ==
ok 1 - sema: iki kitap yaniti da semaya uyar
ok 2 - degismez: oduncte olan kitabin rafta adedi sifirdir
ok 3 - tuketici: rafta olan kitap icin son gun hesaplanir
# pass 3
# fail 0
```

Kırmızıdan yeşile dönüş bu tablonun içindedir. Tür kusuru konduğunda birinci test düşüyor;
katalog servisi raf adedini yeniden tamsayı olarak döndürdüğünde, yani `kusur=yok`
satırında, aynı test yeşile dönüyor. Bu döngü denetimin çalıştığının kanıtıdır.

#### Yakalanan ve Kaçan Sınıflar

Düşen testlerin gerekçesi tek satırda okunabilir.

```bash
for k in tip anlam deger; do
  printf 'kusur=%-6s ' "$k"
  KATALOG_KUSUR=$k node --test --test-reporter=tap api.test.mjs |
    grep -A1 'error: |-' | tail -1 | sed 's/^ *//' | grep . || echo "dusen test yok"
done
```

```
kusur=tip    978-0262033848 -> raftaAdet: tip tamsayi degil (string)
kusur=anlam  978-0201896831 -> durum oduncte ama raftaAdet 3
kusur=deger  dusen test yok
```

**Şema denetiminin yakaladığı sınıf yapısal uyuşmazlıktır:** alanın yokluğu, türünün
değişmesi, desenin ya da değer kümesinin dışına çıkılması. Tür kusuru bu sınıfa girer ve
uyuşmazlık alan adıyla bildirilir. Dersin açılışındaki alan adı değişikliği de aynı sınıftandır;
canlı servise karşı koşan bir şema denetimi onu ilk istekte görür.

**Şema denetiminin kaçırdığı sınıf anlamsal uyuşmazlıktır.** Anlam kusurunda raf adedi hâlâ
sıfırdan büyük bir tamsayı; şemanın bütün kuralları sağlanıyor ve birinci test yeşil kalıyor.
Bu kusuru gören şey şema değil, elle yazılmış bir değişmezdir: ödünçte olan bir kitabın raf
adedi sıfır olmalıdır. Değişmez, iki alan **arasındaki** ilişkiyi söyler; şema ise her alanı
tek tek ele alır. Kaçan sınıfı kapatmanın bedeli budur — her ilişki tek tek yazılır.

Üçüncü kusur ikisinin de dışında kalıyor. Başvuru kitabının ödünç süresi yedi gün olmalıyken
on dört dönüyor; on dört, şemanın kabul ettiği bir tamsayı ve hiçbir değişmezi çiğnemiyor.
Üç testin üçü de yeşil. Burada eksik olan denetim değil **beklenen sonucun kaynağıdır**: bu
kitabın süresinin yedi olduğunu bilen tek yer sözleşmenin kendisidir. Kalite ve Test
Temelleri kursundaki test kâhini sorunu bu noktada yeniden ortaya çıkar ve sonraki iki ders
bu kaynağı iki ayrı yerden getirir.

#### Denetimin Bedeli

Bedel iki türlü ölçülür: koşumdan bağımsız sayılar ve koşum süresinin nereye gittiği.

```js
// bedel.mjs — api testinin bedeli: kac alan denetlendi, kac istek gitti, sure nereye gitti
import { spawn } from "node:child_process";
import { KITAP_SEMA, semayaGore } from "./sema.mjs";

const ISBN = ["978-0262033848", "978-0201896831"];
const surec = spawn("node", ["katalog.mjs", "0"], { env: process.env });
const port = await new Promise((coz) => surec.stdout.once("data", (v) => coz(String(v).split(" ")[1])));
const taban = `http://127.0.0.1:${port}`;
const TEKRAR = 50, govdeler = [];

const t0 = performance.now();
for (let i = 0; i < TEKRAR; i += 1) {
  for (const isbn of ISBN) govdeler.push(await (await fetch(`${taban}/kitap/${isbn}`)).json());
}
const t1 = performance.now();
for (const g of govdeler) semayaGore(KITAP_SEMA, g);
const t2 = performance.now();
surec.kill();

const alan = Object.keys(KITAP_SEMA).length;
const kural = Object.values(KITAP_SEMA).reduce((n, k) => n + Object.keys(k).length, 0);
console.log(`sema: ${alan} alan, ${kural} kural`);
console.log(`kosum: ${govdeler.length} istek, ${govdeler.length * alan} alan denetimi, 2 surec`);
console.log(`istek suresi denetim suresinden buyuk : ${t1 - t0 > t2 - t1}`);
console.log(`oran en az elli kat                   : ${(t1 - t0) / (t2 - t1) >= 50}`);
```

```
sema: 9 alan, 16 kural
kosum: 100 istek, 900 alan denetimi, 2 surec
istek suresi denetim suresinden buyuk : true
oran en az elli kat                   : true
```

Mutlak süreler makineye bağlıdır; bu yüzden çıktı ham milisaniye yerine iki karşılaştırma
bildiriyor. Değişmeyen şey oranın yönüdür: sürenin neredeyse tamamı istek gidip gelmesinde
geçiyor, şema denetiminin kendisi ihmal edilebilir. API testinin bedeli denetimin
karmaşıklığı değil, **süreç dışına çıkma** kararıdır — bu takım iki süreç ayağa kaldırıyor
ve her sınama için ağ üzerinden istek yolluyor.

Koşumdan bağımsız sayılar bakımın ölçüsüdür. Dokuz alan ve on altı kural elle yazılmıştır
ve katalog servisi bir alan eklediğinde şemanın da güncellenmesi gerekir; güncellenmezse
"şemada olmayan alan" satırı testi düşürür. Bu, katı bir seçimdir ve bilinçlidir: sessiz
kalan bir denetim, olmayan bir denetimden ayırt edilemez.

#### Özet

- Kayıt ve tekrar oynatma canlı servise hiç istek göndermez; alan adı değişikliği kayda
  karşı koşan testte görünmez, gerçek istekle koşan bir denetimde ilk çağrıda görünür.
- Şema, yanıtın alanlarını tür, desen, değer kümesi ve sınırla yazılı hâle getirir; dokuz
  alanlık bir kayıt için on altı kural yeterli oldu.
- Şema denetiminin yakaladığı sınıf yapısal uyuşmazlıktır: alanın yokluğu, tür değişimi,
  küme dışı değer. Tür kusuru testi kırmızıya çevirdi, kusur geri alınınca yeşile döndü.
- Şema denetiminin kaçırdığı sınıf anlamsal uyuşmazlıktır: kurallara uyan ama yanlış olan
  değer. İki alan arasındaki ilişkiyi ancak elle yazılmış bir değişmez görür, doğru değerin
  ne olduğunu ise ikisi de bilmez.
- Bedel: iki süreç, sınama başına en az bir istek, elle bakımı yapılan on altı kural; koşum
  süresinin neredeyse tamamı istek gidip gelmesinde geçiyor, denetimin kendisi ihmal edilebilir.

#### Sonraki Adım

Bu dersin şeması katalog servisinin **ürettiği** biçimi tarif ediyor. Ödünç servisinin bu
alanların kaçını gerçekten okuduğu ise hiçbir yerde yazılı değil: dokuz alan denetlendi, oysa
karar işlevi dörde bakıyordu. Fark boşuna değil — sağlayıcı bir alanı kaldırmak istediğinde
"bunu kim okuyor" sorusunun yanıtı şemada yoktur. Sonraki ders beklentiyi tüketicinin
tarafından toplar: tüketicinin çalışan kodunun okuduğu alanlar kaydedilir, bu kayıttan bir
sözleşme üretilir ve sözleşme sağlayıcının takımında koşturulur. Ölçü, tüketicinin gerçekten
kullandığı alan sayısı ile sözleşmenin kapsadığı alan sayısı arasındaki farktır.

### Tüketici Güdümlü Sözleşme Testi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/tuketici-gudumlu-sozlesme-testi

Beklentinin tüketicinin çalışan kodundan toplanıp sağlayıcının takımına taşınması: okunan alanların koşum sırasında kaydedilmesi, üretilen sözleşmenin sağlayıcı tarafında kırıcı değişikliği yakalaması ve çalıştırılmamış dalın sözleşme kapsamı dışında kalmasının ölçülmesi.

Önceki dersin şeması katalog servisinin ürettiği biçimi tarif ediyordu: dokuz alan, on altı
kural. Ödünç servisinin bu alanların kaçını okuduğu ise hiçbir yerde yazılı değildi. Fark
boşuna değildir; sağlayıcı bir alanı kaldırmak istediğinde "bunu kim okuyor" sorusunun
yanıtı şemada bulunmaz.

Bu ders beklentiyi ters yönden toplar. **Tüketici güdümlü sözleşme testinde** beklentiyi
sağlayıcı değil tüketici yazar, sağlayıcı da onu kendi takımında koşturur. Uygulama
Katmanı ve Servis Etkileşimi kursu bu düzenin **kararını** ölçmüştü — kırılan tüketici
sayısı, birlikte yayımlanan birim sayısı, genişlet–daralt sırası. Burada aynı karar bir
teste çevrilir ve iki sayı sorulur: tüketici gerçekten kaç alan okuyor, sözleşme bunların
kaçını kapsıyor.

#### Beklentiyi Tüketicinin Kodundan Toplamak

Beklenti dosyasını elle yazmanın bir kusuru vardır: yazan kişi kodun ne okuduğunu
hatırlamak zorundadır. Ödünç servisinin karar işlevi üç dala ayrılıyor ve her dal farklı
alanlar okuyor.

```js
// odunc.mjs — odunc servisinin karar islevi; katalog kaydindan uc dal ayrilir
export function oduncKarari(kitap, bugun) {
  if (kitap.durum === "kayip")
    return { verilir: false, ileti: `kayip kayit: ${kitap.baslik} / ${kitap.yazar}` };
  if (kitap.durum !== "rafta" || kitap.raftaAdet < 1)
    return { verilir: false, ileti: `rafta yok: ${kitap.isbn}` };
  return { verilir: true, sonGun: bugun + kitap.oduncGun, ileti: `${kitap.isbn} verildi` };
}
```

Katalog servisi önceki dersin sürecidir; bu derste üç kitap taşıyor ve iki kusuru var.
`ad` kusuru ödünç süresi alanını yeniden adlandırır, `eksik` kusuru başlık alanını kaldırır.

```js
// katalog.mjs — katalog servisi; KATALOG_KUSUR degiskeniyle bilerek bozulur
// Kullanim: node katalog.mjs <port|0>   kusur: ad (oduncGun yeniden adlandirilir)
//                                       kusur: eksik (baslik alani kaldirilir)
import { createServer } from "node:http";

if (process.argv[2] === undefined) {
  console.log("kullanim: node katalog.mjs <port|0>");
  process.exit(0);
}

const KUSUR = process.env.KATALOG_KUSUR ?? "yok";
const KITAPLAR = {
  "978-0262033848": { isbn: "978-0262033848", baslik: "Algoritmalara Giris", yazar: "Cormen",
    basimYili: 2009, sube: "merkez", toplamAdet: 4, raftaAdet: 2, durum: "rafta", oduncGun: 14 },
  "978-0201896831": { isbn: "978-0201896831", baslik: "Basvuru El Kitabi", yazar: "Knuth",
    basimYili: 1997, sube: "tepe", toplamAdet: 3, raftaAdet: 0, durum: "oduncte", oduncGun: 7 },
  "978-0134685991": { isbn: "978-0134685991", baslik: "Kayip Cilt", yazar: "Bloch",
    basimYili: 2018, sube: "sahil", toplamAdet: 1, raftaAdet: 0, durum: "kayip", oduncGun: 14 },
};

const disaVer = (k) => {
  if (KUSUR === "ad") { const { oduncGun, ...kalan } = k; return { ...kalan, oduncSuresiGun: oduncGun }; }
  if (KUSUR === "eksik") { const { baslik, ...kalan } = k; return kalan; }
  return { ...k };
};

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;
  const kitap = KITAPLAR[istek.url.split("?")[0].replace("/kitap/", "")];
  yanit.writeHead(kitap ? 200 : 404, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(kitap ? disaVer(kitap) : { hata: "kitap yok" }));
});

sunucu.listen(Number(process.argv[2]), "127.0.0.1", () =>
  console.log(`hazir ${sunucu.address().port} kusur=${KUSUR}`));
```

Beklentiyi hatırlamak yerine **kaydetmek** mümkündür. Tüketicinin okuduğu gövde bir
izleyicinin ardına konur; izleyici her alan okumasını adıyla ve türüyle kaydeder. Aşağıdaki
betik ödünç servisinin kendi test takımındaki iki senaryoyu gerçek katalog servisine karşı
koşturur ve koşumdan bir sözleşme dosyası üretir.

```js
// topla.mjs — tuketicinin kosumundan sozlesme uretir ve kapsamini olcer
import { spawn } from "node:child_process";
import { writeFileSync } from "node:fs";
import { oduncKarari } from "./odunc.mjs";

const surec = spawn("node", ["katalog.mjs", "0"], { env: process.env });
const port = await new Promise((coz) => surec.stdout.once("data", (v) => coz(String(v).split(" ")[1])));
const taban = `http://127.0.0.1:${port}`;
const tipAdi = (d) => (Number.isInteger(d) ? "tamsayi" : typeof d === "string" ? "dizgi" : "diger");

// Tuketici gercek yaniti bir izleyicinin ardindan okur; izleyici okunan adi kaydeder.
async function kos(isbn) {
  const govde = await (await fetch(`${taban}/kitap/${isbn}`)).json();
  const okunan = new Map();
  const izlenen = new Proxy(govde, {
    get: (h, alan) => { if (typeof alan === "string") okunan.set(alan, tipAdi(h[alan])); return h[alan]; },
  });
  return { okunan, karar: oduncKarari(izlenen, 20260) };
}

const GUNLUK = ["978-0262033848", "978-0201896831"];          // odunc servisinin kendi test senaryolari
const KAYIP = "978-0134685991";                                // kayip kitap dali: takimda yok
const sozlesme = { tuketici: "odunc-servisi", etkilesim: [] };

for (const isbn of GUNLUK) {
  const { okunan, karar } = await kos(isbn);
  sozlesme.etkilesim.push({ yol: `/kitap/${isbn}`, alanlar: Object.fromEntries(okunan) });
  console.log(`${isbn} -> ${karar.ileti}\n  okunan alan ${okunan.size}: ${[...okunan.keys()].join(", ")}`);
}
writeFileSync("sozlesme.json", JSON.stringify(sozlesme, null, 1));

const tum = new Set(sozlesme.etkilesim.flatMap((e) => Object.keys(e.alanlar)));
const kapsanan = new Set(tum);
for (const a of (await kos(KAYIP)).okunan.keys()) tum.add(a);
const uretilen = Object.keys(await (await fetch(`${taban}/kitap/${KAYIP}`)).json());
surec.kill();

console.log(`saglayicinin urettigi alan   : ${uretilen.length}`);
console.log(`sozlesmenin kapsadigi alan   : ${kapsanan.size} (${[...kapsanan].join(", ")})`);
console.log(`tuketicinin okuyabildigi alan: ${tum.size} (${[...tum].join(", ")})`);
console.log(`kapsam disinda kalan         : ${[...tum].filter((a) => !kapsanan.has(a)).join(", ")}`);
console.log(`sozlesme dosyasi ${JSON.stringify(sozlesme).length} bayt, ${sozlesme.etkilesim.length} etkilesim`);
```

```
978-0262033848 -> 978-0262033848 verildi
  okunan alan 4: durum, raftaAdet, oduncGun, isbn
978-0201896831 -> rafta yok: 978-0201896831
  okunan alan 2: durum, isbn
saglayicinin urettigi alan   : 9
sozlesmenin kapsadigi alan   : 4 (durum, raftaAdet, oduncGun, isbn)
tuketicinin okuyabildigi alan: 6 (durum, raftaAdet, oduncGun, isbn, baslik, yazar)
kapsam disinda kalan         : baslik, yazar
sozlesme dosyasi 235 bayt, 2 etkilesim
```

İki senaryo iki farklı alan kümesi okudu. İkinci senaryoda yalnız iki alan okunması bir
kaza değil: koşul `durum !== "rafta"` doğru olunca ikinci karşılaştırma hiç çalışmaz, raf
adedi de hiç okunmaz. Sözleşme, kodun **okuyabileceğini** değil, o koşumda **okuduğunu**
yazar.

İlk üç sayı bu dersin ölçüsüdür. Sağlayıcı dokuz alan üretiyor, sözleşme dördünü kapsıyor,
tüketicinin bütün dalları altısını okuyor. Dokuz ile dört arasındaki fark sağlayıcının
serbestlik alanıdır: beş alanı kimse okumuyor. Altı ile dört arasındaki fark ise
sözleşmenin **kör noktasıdır**.

#### Sözleşmenin Sağlayıcı Tarafında Koşması

Sözleşme dosyası tüketicinin deposundan çıkıp sağlayıcının takımına girer. Sağlayıcı, kendi
sürümünü ayağa kaldırır ve dosyadaki her etkileşimi gerçekten yollar.

```js
// saglayici.test.mjs — tuketici sozlesmesi saglayicinin takiminda kosar
// Kullanim: KATALOG_KUSUR=yok|ad|eksik node --test saglayici.test.mjs
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";

const SOZLESME = JSON.parse(readFileSync("sozlesme.json", "utf8"));
const tipAdi = (d) => (Number.isInteger(d) ? "tamsayi" : typeof d === "string" ? "dizgi" : "diger");
let surec, taban;

before(() => new Promise((coz) => {
  surec = spawn("node", ["katalog.mjs", "0"], { env: process.env });
  surec.stdout.once("data", (v) => { taban = `http://127.0.0.1:${String(v).split(" ")[1]}`; coz(); });
}));
after(() => surec.kill());

for (const e of SOZLESME.etkilesim) {
  test(`${SOZLESME.tuketici} ${e.yol}`, async () => {
    const govde = await (await fetch(taban + e.yol)).json();
    const eksik = Object.entries(e.alanlar)
      .filter(([ad, tip]) => govde[ad] === undefined || tipAdi(govde[ad]) !== tip)
      .map(([ad, tip]) => `${ad} (${tip} bekleniyordu)`);
    assert.equal(eksik.length, 0, `karsilanmayan alan: ${eksik.join(", ")}`);
  });
}
```

```bash
for k in yok ad eksik; do
  echo "== kusur=$k =="
  KATALOG_KUSUR=$k node --test --test-reporter=tap saglayici.test.mjs |
    grep -E '^(ok|not ok|# (pass|fail))|karsilanmayan' | sed 's/^ *//'
done
```

```
== kusur=yok ==
ok 1 - odunc-servisi /kitap/978-0262033848
ok 2 - odunc-servisi /kitap/978-0201896831
# pass 2
# fail 0
== kusur=ad ==
not ok 1 - odunc-servisi /kitap/978-0262033848
karsilanmayan alan: oduncGun (tamsayi bekleniyordu)
ok 2 - odunc-servisi /kitap/978-0201896831
# pass 1
# fail 1
== kusur=eksik ==
ok 1 - odunc-servisi /kitap/978-0262033848
ok 2 - odunc-servisi /kitap/978-0201896831
# pass 2
# fail 0
```

**Yakalanan hata sınıfı, sözleşmede yazılı bir alanın kaybolmasıdır.** Ödünç süresi alanı
yeniden adlandırıldığında sınama kırmızıya döndü ve hangi tüketicinin hangi yolunda hangi
alanın karşılanmadığını adıyla bildirdi. Alan eski adına geri konduğunda — `kusur=yok`
satırı — sınama yeşile döndü. Bu sinyalin değeri, sağlayıcının bunu **yayımdan önce, kendi
deposunda** görmesidir; kırılan taraf ise başka bir takımın servisidir.

Bu bilgiyi önceki dersin şema denetimi veremezdi. Şema denetimi "yanıt tanımdan saptı" der;
sözleşme sınaması "ödünç servisinin kitap okuma yolu bozuldu" der.

#### Kapsam Dışında Kalan Dal

Üçüncü sütun sessiz kaldı. Başlık alanı kaldırıldığında iki sınama da geçti, çünkü başlık
sözleşmede yok. Oysa tüketicinin bir dalı onu okuyor.

```js
// sapma.mjs — sozlesmenin yesil kaldigi kusurda tuketicinin kayip dali
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import { oduncKarari } from "./odunc.mjs";

async function kos(kusur, isbn) {
  const surec = spawn("node", ["katalog.mjs", "0"], { env: { ...process.env, KATALOG_KUSUR: kusur } });
  const port = await new Promise((coz) => surec.stdout.once("data", (v) => coz(String(v).split(" ")[1])));
  const govde = await (await fetch(`http://127.0.0.1:${port}/kitap/${isbn}`)).json();
  surec.kill();
  return oduncKarari(govde, 20260);
}

for (const kusur of ["yok", "eksik"]) {
  console.log(`kusur=${kusur.padEnd(6)} kayip dali -> ${(await kos(kusur, "978-0134685991")).ileti}`);
}
const s = JSON.parse(readFileSync("sozlesme.json", "utf8"));
const kapsanan = new Set(s.etkilesim.flatMap((e) => Object.keys(e.alanlar)));
console.log(`sozlesme ${s.etkilesim.length} etkilesim, ${kapsanan.size} alan; baslik kapsamda: ${kapsanan.has("baslik")}`);
```

```
kusur=yok    kayip dali -> kayip kayit: Kayip Cilt / Bloch
kusur=eksik  kayip dali -> kayip kayit: undefined / Bloch
sozlesme 2 etkilesim, 4 alan; baslik kapsamda: false
```

**Kaçan hata sınıfı budur: çalıştırılmamış dalın okuduğu alan.** Sağlayıcının takımı yeşil,
tüketici bozuk. Üretilen sözleşme tüketicinin test takımının bir görüntüsüdür; takım hangi
dalları koşturuyorsa sözleşme onları kapsar. M21/K02'nin kapsam ölçümü burada ikinci bir
anlam kazanır — dal kapsamındaki boşluk, yalnız kendi testinde değil **sağlayıcının
kararında** da boşluk açar.

Eksik beklenti, olmayan bir bağımlılık gibi görünür. Sağlayıcı beş alanın kimse tarafından
okunmadığını sanıyor; gerçekte okunmayan alan sayısı üçtür, çünkü başlık ve yazar
alanlarını okuyan dal sözleşmeye hiç girmedi. Sözleşmenin doğruluğu, onu üreten koşumun
kapsamı kadardır.

#### Sözleşmenin Bedeli

Aynı karar işlevi iki ayrı düzenekte koşturulduğunda bedelin nereden geldiği görünür.

```js
// bedel.mjs — sozlesme dogrulamasinin birim testine gore bedeli
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import { oduncKarari } from "./odunc.mjs";

const SOZLESME = JSON.parse(readFileSync("sozlesme.json", "utf8"));
const TAKLIT = [                                     // taklit katalog: sozlesmenin bellekteki karsiligi
  { isbn: "978-0262033848", durum: "rafta", raftaAdet: 2, oduncGun: 14 },
  { isbn: "978-0201896831", durum: "oduncte", raftaAdet: 0, oduncGun: 7 },
];
const TEKRAR = 50;

const t0 = performance.now();
for (let i = 0; i < TEKRAR; i += 1) for (const k of TAKLIT) oduncKarari(k, 20260);
const t1 = performance.now();

const surec = spawn("node", ["katalog.mjs", "0"], { env: process.env });
const port = await new Promise((coz) => surec.stdout.once("data", (v) => coz(String(v).split(" ")[1])));
for (let i = 0; i < TEKRAR; i += 1) {
  for (const e of SOZLESME.etkilesim) await (await fetch(`http://127.0.0.1:${port}${e.yol}`)).json();
}
const t2 = performance.now();
surec.kill();

const alan = SOZLESME.etkilesim.reduce((n, e) => n + Object.keys(e.alanlar).length, 0);
console.log(`birim kosumu    : 1 bilesen, 0 istek, ${TEKRAR * TAKLIT.length} karar`);
console.log(`sozlesme kosumu : 2 bilesen, ${TEKRAR * SOZLESME.etkilesim.length} istek, ${TEKRAR * alan} alan denetimi`);
console.log(`sozlesme kosumu birim kosumundan uzun : ${t2 - t1 > t1 - t0}`);
console.log(`oran en az yuz kat                    : ${(t2 - t1) / (t1 - t0) >= 100}`);
```

```
birim kosumu    : 1 bilesen, 0 istek, 100 karar
sozlesme kosumu : 2 bilesen, 100 istek, 300 alan denetimi
sozlesme kosumu birim kosumundan uzun : true
oran en az yuz kat                    : true
```

Mutlak süreler makineye bağlıdır; oranın yönü değişmez. Karşılaştırmanın anlamı şudur:
taklit katalogla koşan birim testi ile sözleşme koşumu **aynı dört alana** bakıyor, ama
biri süreç içinde, öteki iki bileşen ve yüz istekle. Ödenen bedelin karşılığı da tek bir
şeydir: taklit katalogdaki dört alanın gerçek servistekilerle aynı olduğu artık
varsayılmıyor, sınanıyor.

Koşumdan bağımsız sayılar bakım tarafını verir. Sözleşme dosyası 235 bayt ve iki etkileşim
taşıyor; elle yazılmadığı için tüketicinin testleri değiştikçe yeniden üretilir. Karşılığında
sağlayıcı, dört alanın okunduğunu **yazılı bir kayda dayanarak** bilir; kalan beş alan için
bildiği tek şey, hiçbir sözleşmede geçmedikleridir.

#### Özet

- Tüketici güdümlü sözleşmede beklentiyi tüketici yazar, sağlayıcı kendi takımında koşturur;
  şema neyin üretildiğini, sözleşme neyin okunduğunu söyler.
- Beklenti elle yazılmak zorunda değildir: tüketicinin koşumu bir izleyicinin ardından
  okunduğunda okunan alanlar adıyla ve türüyle kaydedilir.
- Ölçü üç sayıdır: sağlayıcı dokuz alan üretiyor, sözleşme dördünü kapsıyor, tüketicinin
  bütün dalları altısını okuyor.
- Yakalanan sınıf, sözleşmede yazılı bir alanın kaybolmasıdır; ödünç süresi alanı yeniden
  adlandırıldığında sınama kırmızıya döndü, ad geri konunca yeşile döndü.
- Kaçan sınıf, çalıştırılmamış dalın okuduğu alandır; başlık alanı kaldırıldığında sağlayıcı
  yeşil kaldı ve tüketicinin kayıp kitap dalı bozuldu.
- Bedel: iki bileşen, koşum başına yüz istek, 235 baytlık üretilmiş bir dosya; karşılığında
  taklit kataloğun gerçeğe uyduğu varsayım sınanmış oluyor.

#### Sonraki Adım

Buraya kadar iki denetim de **koşarak** çalıştı: bir süreç kalkıyor, istek gidiyor, yanıt
inceleniyor. Sağlayıcının sorusu ise değişikliği yayımlamadan önce sorulur ve koşum
gerektirmeyen bir biçimi vardır. Bir alan eklemek, bir alan kaldırmak, bir türü daraltmak ve
isteğe bağlı bir alanı zorunlu yapmak — bu dört değişiklik türünün hangisi eski tüketiciyi
kırar, hangisi kırmaz? Sonraki ders bu soruyu iki şema sürümünü karşılaştıran bir denetime
çevirir, değişiklik türlerini uyumluluk sınıflarına dağıtır ve denetimin uyumlu saydığı
hâlde tüketiciyi kıran bir değişikliği aynı koşumda gösterir.

### Şema Uyumluluk Denetimi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/sema-uyumluluk-denetimi

İki şema sürümünü karşılaştıran ve kırıcı değişikliği yayımdan önce durduran denetim: değişiklik türlerinin istek ve yanıt yönünde ayrı uyumluluk sınıflarına düşmesi, genişlet–daralt sırasının kapıyı yeşile çevirmesi ve denetimin uyumlu saydığı bir küme genişletmesinin tüketiciyi kırması.

Önceki iki denetim de koşarak çalıştı: bir süreç kalkıyor, istek gidiyor, yanıt
inceleniyordu. Sağlayıcının asıl sorusu ise değişikliği yayımlamadan önce sorulur ve
koşum gerektirmez. Bir alan eklemek, bir alan kaldırmak, bir türü daraltmak, isteğe bağlı
bir alanı zorunlu yapmak: bu dört değişiklik türünden hangisi eski tüketiciyi kırar?

Kırıcı değişikliğin **tanımı** Web API Tasarımı kursunda, **maliyetinin ölçümü** Uygulama
Katmanı ve Servis Etkileşimi kursunda kuruldu; ikisi de burada tekrarlanmaz. Bu dersin
konusu o bilginin otomatik bir denetime çevrilmesidir: iki şema sürümü karşılaştırılır,
her değişiklik bir türe ve bir uyumluluk sınıfına yerleştirilir, kırıcı sınıfta bir
değişiklik varsa takım kırmızı döner.

#### Şema Sürümleri ve Öneriler

Yayımlanan şema kitap yanıtını ve ayırma isteğini birlikte tarif ediyor. Üç öneri var:
`A` dört değişiklik türünü bir arada taşıyor, `B` aynı ihtiyacı genişlet–daralt sırasının
birinci adımıyla karşılıyor, `C` yalnız bir değer kümesini genişletiyor.

```js
// sema.mjs — yayimlanan sema ve uc oneri; her alan tip, zorunluluk ve deger kumesi tasir
const A = (tip, zorunlu, kume) => (kume ? { tip, zorunlu, kume } : { tip, zorunlu });

export const YAYIN = {
  yanit: {
    isbn: A("dizgi", true), baslik: A("dizgi", true), raftaAdet: A("sayi", true),
    durum: A("dizgi", true, ["rafta", "oduncte", "kayip"]),
    oduncGun: A("sayi", true), etiketler: A("dizi", false),
  },
  istek: { uyeNo: A("dizgi", true), sube: A("dizgi", false), adet: A("sayi", false) },
};

// A: dort degisiklik turu bir arada. B: genislet adimi. C: yalniz kume genisler.
export const ONERI = {
  A: {
    yanit: { ...YAYIN.yanit, sonGun: A("dizgi", false) },
    istek: { ...YAYIN.istek, sube: A("dizgi", true), adet: A("tamsayi", false) },
  },
  B: { yanit: { ...YAYIN.yanit, sonGun: A("dizgi", false) }, istek: { ...YAYIN.istek } },
  C: {
    yanit: { ...YAYIN.yanit, durum: A("dizgi", true, ["rafta", "oduncte", "kayip", "rafta-yeni"]) },
    istek: { ...YAYIN.istek },
  },
};
delete ONERI.A.yanit.etiketler;                       // alan kaldirma
```

Denetimin kendisi iki parçadır: farkı türlere ayıran bir karşılaştırma ve türleri sınıflara
bağlayan bir tablo. Tablonun iki sütunu olması bu dersin ana ayrımıdır — aynı değişiklik
türü istek yönünde ve yanıt yönünde farklı sınıfa düşer.

```js
// uyumluluk.mjs — iki sema surumu arasindaki degisiklikleri turlerine ve siniflarina ayirir
const DAR = { sayi: "tamsayi" };

export const SINIF = {
  "istege bagli alan ekleme": { yanit: "uyumlu", istek: "uyumlu" },
  "zorunlu alan ekleme": { yanit: "uyumlu", istek: "kirici" },
  "alan kaldirma": { yanit: "kirici", istek: "uyumlu" },
  "tur daraltma": { yanit: "uyumlu", istek: "kirici" },
  "tur genisletme": { yanit: "kirici", istek: "uyumlu" },
  "zorunlu yapma": { yanit: "uyumlu", istek: "kirici" },
  "istege bagli yapma": { yanit: "kirici", istek: "uyumlu" },
  "kumeye deger ekleme": { yanit: "uyumlu", istek: "kirici" },
  "kumeden deger cikarma": { yanit: "kirici", istek: "uyumlu" },
};

export function fark(eski, yeni) {
  const d = [];
  for (const [ad, k] of Object.entries(eski)) {
    const y = yeni[ad];
    if (y === undefined) { d.push({ ad, tur: "alan kaldirma" }); continue; }
    if (y.tip !== k.tip) d.push({ ad, tur: DAR[k.tip] === y.tip ? "tur daraltma" : "tur genisletme" });
    if (y.zorunlu && !k.zorunlu) d.push({ ad, tur: "zorunlu yapma" });
    if (k.zorunlu && !y.zorunlu) d.push({ ad, tur: "istege bagli yapma" });
    if ((y.kume ?? []).some((v) => !(k.kume ?? []).includes(v))) d.push({ ad, tur: "kumeye deger ekleme" });
    if ((k.kume ?? []).some((v) => !(y.kume ?? []).includes(v))) d.push({ ad, tur: "kumeden deger cikarma" });
  }
  for (const [ad, y] of Object.entries(yeni)) {
    if (ad in eski) continue;
    d.push({ ad, tur: y.zorunlu ? "zorunlu alan ekleme" : "istege bagli alan ekleme" });
  }
  return d;
}

// Bir oneriyi iki yonde de degerlendirir; donen listenin her satiri siniflandirilmis bir degisikliktir.
export const degerlendir = (yayin, oneri) => ["yanit", "istek"].flatMap((yon) =>
  fark(yayin[yon], oneri[yon]).map((x) => ({ yon, ...x, sinif: SINIF[x.tur][yon] })));
```

#### Türlerin Sınıflara Dağılımı

```js
// dagilim.mjs — degisiklik turlerinin uyumluluk siniflarina dagilimi ve uc onerinin sayimi
import { YAYIN, ONERI } from "./sema.mjs";
import { SINIF, degerlendir } from "./uyumluluk.mjs";

console.log(`${"degisiklik turu".padEnd(26)}${"yanitta".padStart(9)}${"istekte".padStart(9)}`);
for (const [tur, s] of Object.entries(SINIF)) {
  console.log(`${tur.padEnd(26)}${s.yanit.padStart(9)}${s.istek.padStart(9)}`);
}

for (const [ad, o] of Object.entries(ONERI)) {
  const d = degerlendir(YAYIN, o);
  console.log(`\noneri ${ad}: ${d.length} degisiklik, ${d.filter((x) => x.sinif === "kirici").length} kirici`);
  for (const x of d) console.log(`  ${`${x.yon}.${x.ad}`.padEnd(17)}${x.tur.padEnd(26)}-> ${x.sinif}`);
}
```

```
degisiklik turu             yanitta  istekte
istege bagli alan ekleme     uyumlu   uyumlu
zorunlu alan ekleme          uyumlu   kirici
alan kaldirma                kirici   uyumlu
tur daraltma                 uyumlu   kirici
tur genisletme               kirici   uyumlu
zorunlu yapma                uyumlu   kirici
istege bagli yapma           kirici   uyumlu
kumeye deger ekleme          uyumlu   kirici
kumeden deger cikarma        kirici   uyumlu

oneri A: 4 degisiklik, 3 kirici
  yanit.etiketler  alan kaldirma             -> kirici
  yanit.sonGun     istege bagli alan ekleme  -> uyumlu
  istek.sube       zorunlu yapma             -> kirici
  istek.adet       tur daraltma              -> kirici

oneri B: 1 degisiklik, 0 kirici
  yanit.sonGun     istege bagli alan ekleme  -> uyumlu

oneri C: 1 degisiklik, 0 kirici
  yanit.durum      kumeye deger ekleme       -> uyumlu
```

Tablonun iki sütunu birbirinin aynası. Yanıtta bir alanı kaldırmak kırıcıdır, çünkü
tüketici onu okuyordur; istekte bir alanı kaldırmak kırıcı değildir, çünkü eski istemci
göndermeye devam eder ve alan yok sayılır. Aynı biçimde bir alanı zorunlu yapmak yanıtta
zarar vermez — alan zaten hep gelecektir — istekte ise eski istemcinin gönderdiği bütün
isteklerin reddedilmesine yol açar. Tür daraltmak yanıtta uyumludur, çünkü tamsayı hâlâ bir
sayıdır; istekte kırıcıdır, çünkü eski istemci ondalık gönderebilir.

Bu simetri, sekiz satırlık tablonun ezberlenmesi gereken bir liste olmadığını gösterir.
Tek bir soru vardır ve yön onu belirler: **değişiklikten sonra karşı tarafın elindeki eski
kod hâlâ geçerli mi.** Yanıtta karşı taraf okur, istekte yazar.

#### Kabul Kapısı

Denetim, Kalite ve Test Temelleri kursunda tanımlanan **kabul kapısı** olarak çalışır:
kırıcı sınıfta bir değişiklik varsa yayın durur.

```js
// uyumluluk.test.mjs — yayin oncesi kapi: kirici degisiklik varsa takim kirmizi doner
// Kullanim: ONERI=A|B|C node --test uyumluluk.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";
import { YAYIN, ONERI } from "./sema.mjs";
import { degerlendir } from "./uyumluluk.mjs";

const AD = process.env.ONERI ?? "B";

test(`oneri ${AD}: yayimlanan semaya gore kirici degisiklik yok`, () => {
  const kirici = degerlendir(YAYIN, ONERI[AD]).filter((x) => x.sinif === "kirici");
  assert.equal(kirici.length, 0,
    `kirici degisiklik: ${kirici.map((x) => `${x.yon}.${x.ad} ${x.tur}`).join(", ")}`);
});
```

```bash
for o in A B C; do
  ONERI=$o node --test --test-reporter=tap uyumluluk.test.mjs |
    grep -E '^(ok|not ok)|kirici degisiklik:' | sed 's/^ *//'
done
```

```
not ok 1 - oneri A: yayimlanan semaya gore kirici degisiklik yok
kirici degisiklik: yanit.etiketler alan kaldirma, istek.sube zorunlu yapma, istek.adet tur daraltma
ok 1 - oneri B: yayimlanan semaya gore kirici degisiklik yok
ok 1 - oneri C: yayimlanan semaya gore kirici degisiklik yok
```

**Yakalanan hata sınıfı, sözleşmenin yayımdan önce kırılmasıdır.** Öneri A kırmızı döndü
ve üç kırıcı değişikliği yön bilgisiyle birlikte adlandırdı. Düzeltme, aynı ihtiyacı
genişlet–daralt sırasının birinci adımına çevirmektir: yeni alan eskilerin yanına eklenir,
kaldırma ve zorunlu yapma üçüncü adıma bırakılır. Öneri B tam olarak budur ve kapı yeşile
döner.

Bu denetimin önceki iki dersten farkı, kırılmayı **hiçbir tüketici çalışmadan** görmesidir.
Sözleşme testi kırılmayı gösterebilmek için tüketicinin beklentisini ve sağlayıcının
sürümünü birlikte koşturmak zorundaydı; buradaki denetim iki metni karşılaştırıyor.
Karşılığında bildiği şey daha azdır: hangi tüketicinin kırıldığını söyleyemez, yalnız
birinin kırılabileceğini söyler.

#### Denetimin Uyumlu Saydığı Kırılma

Öneri C kapıdan geçti. Kitap durumu için tanımlı değer kümesine yeni bir değer eklendi:
depoya yeni giren ve rafta duran kitaplar artık `rafta-yeni` olarak bildiriliyor. Eski
yanıtların hepsi hâlâ geçerli, hiçbir alan kaybolmadı, hiçbir tür değişmedi.

```js
// kacan.mjs — denetimin uyumlu saydigi degisiklik tuketiciyi kiriyor
import { YAYIN, ONERI } from "./sema.mjs";
import { degerlendir } from "./uyumluluk.mjs";

// Odunc servisinin karar dali: bilinmeyen durum degeri "rafta degil" sayilir.
const oduncKarari = (k) => (k.durum === "rafta" && k.raftaAdet > 0
  ? { verilir: true, ileti: `${k.isbn} verildi` }
  : { verilir: false, ileti: `rafta yok (durum ${k.durum}, raftaAdet ${k.raftaAdet})` });

const d = degerlendir(YAYIN, ONERI.C);
console.log(`denetim: oneri C -> ${d.length} degisiklik, ${d.filter((x) => x.sinif === "kirici").length} kirici, kapi yesil`);

const RAF = { isbn: "978-0262033848", durum: "rafta", raftaAdet: 2 };
const YENI = { ...RAF, durum: "rafta-yeni" };          // yeni gelen raf: kitap rafta ve odunc verilebilir
for (const [ad, k] of [["eski deger", RAF], ["yeni deger", YENI]]) {
  const karar = oduncKarari(k);
  console.log(`${ad.padEnd(11)} -> verilir=${String(karar.verilir).padEnd(5)} ${karar.ileti}`);
}
console.log(`semadaki durum degeri ${ONERI.C.yanit.durum.kume.length}, tuketicinin odunc verilebilir saydigi 1`);
console.log(`birim degisikligi semayi degistirmez -> ${degerlendir(YAYIN, YAYIN).length} degisiklik`);
```

```
denetim: oneri C -> 1 degisiklik, 0 kirici, kapi yesil
eski deger  -> verilir=true  978-0262033848 verildi
yeni deger  -> verilir=false rafta yok (durum rafta-yeni, raftaAdet 2)
semadaki durum degeri 4, tuketicinin odunc verilebilir saydigi 1
birim degisikligi semayi degistirmez -> 0 degisiklik
```

**Kaçan hata sınıfı, biçimi bozmadan anlamı değiştiren değişikliktir.** Rafta duran bir
kitap ödünç verilemez oldu; şema açısından hiçbir kural çiğnenmedi. Kümeye değer eklemek
yanıt yönünde uyumlu sayılır, çünkü şema doğrulaması açısından öyledir — eski değerler
geçerliliğini korur. Kırılan şey şema değil, tüketicinin o kümeyi **tükettiğini varsayan**
kodudur. Bu varsayım hiçbir şemada yazılı değildir.

Son satır sınırı daha da net gösteriyor. Ödünç süresinin gün yerine hafta sayılmaya
başlaması, alan adını da türünü de değiştirmez; iki şema birebir aynı kalır ve denetim
sıfır değişiklik bildirir. Şemaya bakan hiçbir denetim birim, ölçek ya da anlam
değişikliğini göremez.

#### Denetimin Bedeli

```js
// bedel.mjs — semaya bakan denetimin bedeli: sifir surec, sifir istek
import { createServer } from "node:http";
import { YAYIN, ONERI } from "./sema.mjs";
import { degerlendir } from "./uyumluluk.mjs";

const TEKRAR = 200, oneri = Object.keys(ONERI).length;
const t0 = performance.now();
for (let i = 0; i < TEKRAR; i += 1) for (const o of Object.values(ONERI)) degerlendir(YAYIN, o);
const t1 = performance.now();

// Karsilastirma noktasi: ayni makinede tek bir yerel istegin gidip gelmesi.
const sunucu = createServer((_, y) => y.end("{}"));
await new Promise((c) => sunucu.listen(0, "127.0.0.1", c));
const t2 = performance.now();
await (await fetch(`http://127.0.0.1:${sunucu.address().port}/`)).text();
const t3 = performance.now();
sunucu.close();

const alan = Object.keys(YAYIN.yanit).length + Object.keys(YAYIN.istek).length;
console.log(`sema ${alan} alan (yanit ${Object.keys(YAYIN.yanit).length}, istek ${Object.keys(YAYIN.istek).length}), ${oneri} oneri`);
console.log(`denetim ${TEKRAR * oneri} degerlendirme, 0 surec, 0 istek, 0 test verisi`);
console.log(`bir degerlendirme tek bir yerel istekten kisa : ${(t1 - t0) / (TEKRAR * oneri) < t3 - t2}`);
console.log(`oran en az elli kat                           : ${(t3 - t2) / ((t1 - t0) / (TEKRAR * oneri)) >= 50}`);
```

```
sema 9 alan (yanit 6, istek 3), 3 oneri
denetim 600 degerlendirme, 0 surec, 0 istek, 0 test verisi
bir degerlendirme tek bir yerel istekten kisa : true
oran en az elli kat                           : true
```

Mutlak süreler makineye bağlıdır; oranın yönü değişmez. Bu denetim hiçbir süreç ayağa
kaldırmıyor, hiçbir istek yollamıyor ve hiçbir test verisi hazırlamıyor: girdisi iki metin,
çıktısı bir sınıflandırılmış liste. Tek bir yerel isteğin süresinde yüzlerce değerlendirme
yapılabilir; bu yüzden denetim her değişiklik önerisinde koşturulabilir.

Koşumdan bağımsız bedel iki yerdedir. Birincisi, şemanın **yayımlanan** sürümünün bir
yerde saklanması gerekliliğidir: karşılaştırma iki metin ister, tek metinle uyumluluk
sorusu sorulamaz. İkincisi, sınıf tablosunun bakımıdır — dokuz satır elle yazılmıştır ve
şema diline yeni bir kısıt eklendiğinde tabloya da satır eklenmelidir. Eksik satır sessiz
kalır ve sessiz kalan bir denetim, olmayan bir denetimden ayırt edilemez.

#### Özet

- Şema uyumluluk denetimi iki şema sürümünü karşılaştırır, farkı değişiklik türlerine
  ayırır ve her türü bir uyumluluk sınıfına yerleştirir.
- Aynı tür istek ve yanıt yönünde farklı sınıfa düşer; tek soru, değişiklikten sonra karşı
  taraftaki eski kodun hâlâ geçerli olup olmadığıdır.
- Dört değişiklik türünü bir arada taşıyan öneri üç kırıcı değişiklikle kırmızı döndü;
  aynı ihtiyaç genişlet–daralt sırasının birinci adımına çevrildiğinde kapı yeşile döndü.
- Kaçan sınıf biçimi bozmadan anlamı değiştiren değişikliktir: değer kümesine eklenen bir
  değer uyumlu sayıldı, oysa rafta duran kitap ödünç verilemez oldu. Birim değişikliğinde
  iki şema birebir aynı kalır ve denetim sıfır değişiklik bildirir.
- Bedel: sıfır süreç, sıfır istek, sıfır test verisi; karşılığında yayımlanan şemanın
  saklanması ve dokuz satırlık sınıf tablosunun elle bakımı gerekir.
- Denetim hangi tüketicinin kırıldığını söyleyemez; yalnız birinin kırılabileceğini söyler.

#### Sonraki Adım

Üç denetim de sağlayıcının hazır olduğunu varsaydı: şema denetimi canlı servise istek
yolladı, sözleşme testi sağlayıcının sürümünü ayağa kaldırdı, uyumluluk denetimi yayımlanan
şemayı okudu. Oysa sözleşmenin en çok işe yaradığı an, sağlayıcının **henüz yazılmadığı**
andır. Tüketici takımı beklemek zorunda değildir, çünkü sözleşme bellidir. Sonraki ders
sözleşmeden yanıt üreten bir sahte sunucu kurar, ödünç servisini ona karşı geliştirir ve
tek bir soruyu ölçer: sahte sunucu gerçeğinden kaç noktada sapıyor ve bu sapmaların kaçını
buraya kadar kurulan denetimler kapatabiliyor.

### Sahte Sunucularla Geliştirme

Source: https://academia.sh/tr/kurslar/tumlesim-testi/sahte-sunucularla-gelistirme

Sağlayıcı hazır olmadan geliştirmeyi sürdüren sahte sunucunun gerçeğinden saptığı noktaların sayılması: aynı istek kümesinin iki sunucuya sürülmesi, sözleşme koşumunun kapattığı sapmanın ölçülmesi ve istekler arası duruma bakan sapmanın hiçbir sözleşmeyle kapatılamaması.

Önceki üç denetim de sağlayıcının hazır olduğunu varsaydı. Oysa sözleşmenin en çok işe
yaradığı an, sağlayıcının **henüz yazılmadığı** andır: ödünç servisini geliştiren takım
katalog servisinin bitmesini beklemek zorunda değildir, çünkü sözleşme bellidir.

**Sahte sunucu** (mock server) sözleşmeyi çalıştırılabilir hâle getirir ve bekleme süresini
kaldırır. Karşılığında bir risk üretir: gerçeğin yerine geçen şey gerçeğin kendisi değildir.
Bu ders o riski bir sayıya çevirir — sahte sunucu gerçeğinden **kaç noktada sapıyor** ve bu
sapmaların kaçını buraya kadar kurulan sözleşme koşumu kapatabiliyor.

#### İki Sunucu, Tek Sözleşme

Sözleşme önceki derslerin ürettiği bilgidir: ödünç servisinin okuduğu beş alan, tipleriyle
birlikte. Sahte sunucunun üreteceği örnek değerler de aynı dosyada durur.

```js
// sozlesme.mjs — kitap yanitinin sozlesmesi: okunan alanlar, tipleri ve ornek degerler
export const ALAN = { isbn: "dizgi", baslik: "dizgi", raftaAdet: "tamsayi", durum: "dizgi", oduncGun: "tamsayi" };
export const ORNEK = { isbn: "978-0262033848", baslik: "Ornek Kitap", raftaAdet: 2, durum: "rafta", oduncGun: 14 };
export const ORNEK_V1 = { ...ORNEK, raftaAdet: "2" };   // sahte sunucunun ilk surumu: ornek deger dizgi
export const ETKILESIM = [{ yol: "/kitap/978-0262033848", alanlar: ALAN }];
```

Gerçek katalog servisi iki kitap taşıyor, ayırma isteğinde raf adedini düşürüyor ve rafta
kitap yoksa isteği reddediyor.

```js
// gercek.mjs — gercek katalog servisi: durum tutar, is kuralini uygular
// Kullanim: node gercek.mjs <port|0>
import { createServer } from "node:http";
if (process.argv[2] === undefined) { console.log("kullanim: node gercek.mjs <port|0>"); process.exit(0); }

const KITAP = {
  "978-0262033848": { isbn: "978-0262033848", baslik: "Algoritmalara Giris", raftaAdet: 2,
    durum: "rafta", oduncGun: 14, etiketler: ["algoritma"] },
  "978-0201896831": { isbn: "978-0201896831", baslik: "Basvuru El Kitabi", raftaAdet: 0,
    durum: "oduncte", oduncGun: 7, etiketler: [] },
};
const govdeOku = (i) => new Promise((c) => { let v = ""; i.on("data", (p) => (v += p)); i.on("end", () => c(v)); });

const s = createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];
  const json = (kod, g) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
    yanit.end(JSON.stringify(g));
  };
  if (istek.method === "POST" && yol === "/ayirma") {
    const k = KITAP[JSON.parse((await govdeOku(istek)) || "{}").isbn];
    if (k === undefined) return json(404, { hata: "kitap yok" });
    if (k.raftaAdet < 1) return json(409, { hata: "rafta bos" });
    k.raftaAdet -= 1;
    return json(201, { ayirmaNo: "A-1", isbn: k.isbn });
  }
  const k = KITAP[yol.replace("/kitap/", "")];
  return k ? json(200, k) : json(404, { hata: "kitap yok" });
});
s.listen(Number(process.argv[2]), "127.0.0.1", () => console.log(`gercek ${s.address().port}`));
```

Sahte sunucu sözleşmeden üretilir ve sözleşmede yazılı olan her şeyi yapar: örnek gövdeyi
döndürür. Sözleşmede yazılı olmayan hiçbir şeyi yapmaz — durum tutmaz, kural uygulamaz.

```js
// sahte.mjs — sozlesmeden uretilen sahte sunucu: durum tutmaz, is kurali uygulamaz
// Kullanim: SAHTE_SURUM=1|2 node sahte.mjs <port|0>
import { createServer } from "node:http";
import { ORNEK, ORNEK_V1 } from "./sozlesme.mjs";
if (process.argv[2] === undefined) { console.log("kullanim: node sahte.mjs <port|0>"); process.exit(0); }

const GOVDE = process.env.SAHTE_SURUM === "2" ? ORNEK : ORNEK_V1;
const s = createServer((istek, yanit) => {
  istek.resume();
  yanit.sendDate = false;
  const ayirma = istek.url.split("?")[0] === "/ayirma";
  yanit.writeHead(ayirma ? 201 : 200, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(ayirma ? { ayirmaNo: "A-1", isbn: GOVDE.isbn } : GOVDE));
});
s.listen(Number(process.argv[2]), "127.0.0.1", () => console.log(`sahte ${s.address().port}`));
```

#### Sapma Noktalarının Sayımı

Sapma bir görüş değil, bir sayıdır: aynı istek kümesi iki sunucuya da sürülür ve beş ölçüt
karşılaştırılır.

```js
// sapma.mjs — ayni istek kumesi iki sunucuya surulur, sapma noktalari sayilir
import { spawn } from "node:child_process";
import { ALAN } from "./sozlesme.mjs";

const ISBN = "978-0262033848";
const tipAdi = (d) => (Number.isInteger(d) ? "tamsayi" : typeof d === "string" ? "dizgi" : "diger");

async function surucu(dosya) {
  const surec = spawn("node", [dosya, "0"], { env: process.env });
  const port = await new Promise((c) => surec.stdout.once("data", (v) => c(String(v).split(" ")[1])));
  const taban = `http://127.0.0.1:${port}`;
  const al = async (yol, secenek) => {
    const c = await fetch(taban + yol, secenek);
    return { kod: c.status, govde: await c.json() };
  };
  const kitap = await al(`/kitap/${ISBN}`);
  const yok = await al("/kitap/978-0000000000");
  const bos = await al("/ayirma", { method: "POST", body: JSON.stringify({ isbn: "978-0201896831" }) });
  await al("/ayirma", { method: "POST", body: JSON.stringify({ isbn: ISBN }) });
  const sonra = await al(`/kitap/${ISBN}`);
  surec.kill();
  return {
    "basari alan sayisi": String(Object.keys(kitap.govde).length),
    "raftaAdet tipi": tipAdi(kitap.govde.raftaAdet),
    "bilinmeyen kitap kodu": String(yok.kod),
    "rafta bos kitapta ayirma": String(bos.kod),
    "ayirma sonrasi raftaAdet": String(sonra.govde.raftaAdet),
  };
}

const sahte = await surucu("sahte.mjs");
const gercek = await surucu("gercek.mjs");
console.log(`${"olcut".padEnd(26)}${"sahte".padStart(9)}${"gercek".padStart(9)}${"sapma".padStart(7)}`);
let sapma = 0;
for (const olcut of Object.keys(sahte)) {
  const fark = sahte[olcut] !== gercek[olcut];
  sapma += fark ? 1 : 0;
  console.log(`${olcut.padEnd(26)}${sahte[olcut].padStart(9)}${gercek[olcut].padStart(9)}` +
    `${(fark ? "evet" : "hayir").padStart(7)}`);
}
console.log(`sahte surum ${process.env.SAHTE_SURUM ?? "1"}: sapma ${sapma}/${Object.keys(sahte).length}, ` +
  `sozlesmenin denetledigi alan ${Object.keys(ALAN).length}`);
```

```
olcut                         sahte   gercek  sapma
basari alan sayisi                5        6   evet
raftaAdet tipi                dizgi  tamsayi   evet
bilinmeyen kitap kodu           200      404   evet
rafta bos kitapta ayirma        201      409   evet
ayirma sonrasi raftaAdet          2        1   evet
sahte surum 1: sapma 5/5, sozlesmenin denetledigi alan 5
```

Beş ölçütün beşi de sapıyor. Sapmalar aynı türden değildir ve ayrımı bu dersin ekseni.
İlk ikisi **biçim** sapmasıdır: alan sayısı ve tip. Üçüncü ve dördüncü **karar** sapmasıdır:
sahte sunucu her isteğe olumlu yanıt verir, çünkü hangi durumda ne döneceğini bilmez.
Beşincisi **durum** sapmasıdır: gerçek servis ayırmadan sonra raf adedini düşürür, sahte
sunucu iki isteği birbirinden bağımsız görür.

#### Sözleşme Koşumunun Kapattığı Sapma

Sahte sunucu da sözleşme koşumundan geçmelidir. Aynı sınama dosyası hem sahteye hem gerçeğe
karşı koşturulur; sunucu adı dışarıdan verilir.

```js
// sozlesme.test.mjs — ayni sozlesme kosumu hem sahte hem gercek sunucuya karsi
// Kullanim: SUNUCU=sahte.mjs|gercek.mjs SAHTE_SURUM=1|2 node --test sozlesme.test.mjs
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { ETKILESIM } from "./sozlesme.mjs";

const DOSYA = process.env.SUNUCU ?? "gercek.mjs";
const tipAdi = (d) => (Number.isInteger(d) ? "tamsayi" : typeof d === "string" ? "dizgi" : "diger");
let surec, taban;

before(() => new Promise((coz) => {
  surec = spawn("node", [DOSYA, "0"], { env: process.env });
  surec.stdout.once("data", (v) => { taban = `http://127.0.0.1:${String(v).split(" ")[1]}`; coz(); });
}));
after(() => surec.kill());

for (const e of ETKILESIM) {
  test(`${DOSYA} surum ${process.env.SAHTE_SURUM ?? "-"} ${e.yol}`, async () => {
    const govde = await (await fetch(taban + e.yol)).json();
    const eksik = Object.entries(e.alanlar)
      .filter(([ad, tip]) => govde[ad] === undefined || tipAdi(govde[ad]) !== tip)
      .map(([ad, tip]) => `${ad} (${tip} bekleniyordu)`);
    assert.equal(eksik.length, 0, `karsilanmayan alan: ${eksik.join(", ")}`);
  });
}
```

```bash
for s in "sahte.mjs 1" "sahte.mjs 2" "gercek.mjs -"; do
  set -- $s
  SUNUCU=$1 SAHTE_SURUM=$2 node --test --test-reporter=tap sozlesme.test.mjs |
    grep -E '^(ok|not ok)|karsilanmayan' | sed 's/^ *//'
done
SAHTE_SURUM=2 node sapma.mjs | tail -1
```

```
not ok 1 - sahte.mjs surum 1 /kitap/978-0262033848
karsilanmayan alan: raftaAdet (tamsayi bekleniyordu)
ok 1 - sahte.mjs surum 2 /kitap/978-0262033848
ok 1 - gercek.mjs surum - /kitap/978-0262033848
sahte surum 2: sapma 4/5, sozlesmenin denetledigi alan 5
```

**Yakalanan hata sınıfı, sahte sunucunun sözleşmeden sapmasıdır.** Sahte sunucunun ilk
sürümünde örnek değer dizgi olarak yazılmıştı; sözleşme koşumu kırmızıya döndü ve alanı
adıyla bildirdi. Örnek düzeltildikten sonra aynı koşum yeşile döndü ve sapma sayısı beşten
dörde indi. Buradan çıkan kural tek cümledir: **sahte sunucu, tüketicinin sınandığı
sözleşmenin aynısıyla sınanmalıdır**; yoksa tüketici sözleşmeye değil sahte sunucunun
kendisine göre geliştirilmiş olur.

**Kaçan hata sınıfı, sözleşmenin görüş alanı dışındaki sapmalardır ve üçe ayrılır.**
Birincisi alan sayısı: gerçek servis etiket alanını da döndürüyor, sahte sunucu
döndürmüyor. Sözleşme onu kapsamıyor, çünkü tüketici okumuyor; bir gün okumaya başlarsa
önceki dersin kayıt düzeni onu sözleşmeye ekler ve sapma kendiliğinden kapanır.

İkincisi karar sapması: bilinmeyen kitapta durum kodu ve rafta kitap yokken ayırma isteği.
Bunlar sözleşmeye **yeni etkileşim** eklenerek kapatılabilir; bedeli her biri için bir
senaryo ve sahte sunucuya bir kural yazmaktır. Sahte sunucu her kapatılan sapmada gerçek
servise biraz daha benzer; sınırda sahte sunucu gerçek servisin ikinci bir uygulamasına
dönüşür ve o noktada var olma nedenini yitirir.

Üçüncüsü kapatılamaz. Ayırmadan sonra raf adedinin düşmesi tek bir isteğin özelliği
değildir; **iki isteğin arasındaki ilişkidir**. Sözleşme testinin baktığı birim bir istek ve
onun yanıtıdır; istekler arası durum bu birimin dışında kalır. Sahte sunucuya durum
tutturmak bu sapmayı kapatmaz, yalnız yerini değiştirir — bu kez sahte sunucunun durum
kuralı gerçeğinden sapabilir ve onu sınayacak bir sözleşme yoktur.

#### Sahte Sunucunun Bedeli

```js
// bedel.mjs — sahte sunucunun bedeli: kac satir duzenek, kac istek, hangi sure
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";

const satir = (d) => readFileSync(d, "utf8").trimEnd().split("\n").length;

async function olc(dosya) {
  const surec = spawn("node", [dosya, "0"], { env: { ...process.env, SAHTE_SURUM: "2" } });
  const port = await new Promise((c) => surec.stdout.once("data", (v) => c(String(v).split(" ")[1])));
  const yol = `http://127.0.0.1:${port}/kitap/978-0262033848`;
  for (let i = 0; i < 20; i += 1) await (await fetch(yol)).json();      // isinma
  const t = performance.now();
  for (let i = 0; i < 100; i += 1) await (await fetch(yol)).json();
  const sure = performance.now() - t;
  surec.kill();
  return sure;
}

const s = await olc("sahte.mjs");
const g = await olc("gercek.mjs");
console.log(`duzenek: sahte.mjs ${satir("sahte.mjs")} satir, gercek.mjs ${satir("gercek.mjs")} satir`);
console.log(`sapma kosumu 2 surec ve 10 istek, sozlesme kosumu 1 surec ve 1 etkilesim`);
console.log(`iki sunucunun sureleri ayni buyukluk sirasinda : ${s / g > 0.33 && s / g < 3}`);
console.log(`sahte sunucu kosumu gercegin yarisindan kisa   : ${s < g / 2}`);
```

```
duzenek: sahte.mjs 15 satir, gercek.mjs 31 satir
sapma kosumu 2 surec ve 10 istek, sozlesme kosumu 1 surec ve 1 etkilesim
iki sunucunun sureleri ayni buyukluk sirasinda : true
sahte sunucu kosumu gercegin yarisindan kisa   : false
```

Son satır bir yanlış anlamayı düzeltiyor. Sahte sunucu **hız** için kullanılmaz: iki sunucu
da yerel bir süreçtir ve yüz isteğin süresi aynı büyüklük sırasındadır. Sahte sunucunun
sağladığı şey süre değil **zamanlamadır** — sağlayıcı hazır olmadan tüketicinin
geliştirilebilmesi. Bu ayrım, sahte sunucunun gerçek servis yerine sürekli kullanılmasının
gerekçesiz olduğunu da söyler.

Koşumdan bağımsız bedel iki sayıdadır. Düzenek olarak sahte sunucu on beş satır, gerçek
servis otuz bir satır; aradaki on altı satır tam olarak sahte sunucunun **yapmadığı**
işlerdir ve sapma tablosundaki beş satırın karşılığıdır. İkinci sayı sapma tablosunun
kendisidir: beş ölçüt, koşum başına iki süreç ve on istek. Bu tablo bir kere yazılır ve
sözleşme her değiştiğinde koşturulur; sahte sunucunun gerçekten ne kadar saptığını tahmin
etmenin başka yolu yoktur.

#### Özet

- Sahte sunucu sözleşmeyi çalıştırılabilir hâle getirir ve tüketicinin sağlayıcıyı
  beklemeden geliştirilmesini sağlar; karşılığında gerçeğinden sapma riski üretir.
- Sapma bir sayıdır: aynı istek kümesi iki sunucuya sürüldüğünde beş ölçütün beşi de saptı.
- Sapmalar üç türdür — biçim (alan sayısı, tip), karar (durum kodu, kural), durum (istekler
  arası etki).
- Sahte sunucu tüketiciyle aynı sözleşmeye karşı sınandığında biçim sapması yakalandı;
  örnek değer düzeltilince koşum yeşile döndü ve sapma beşten dörde indi.
- Karar sapmaları sözleşmeye etkileşim eklenerek kapatılabilir; her kapatma sahte sunucuyu
  gerçeğe yaklaştırır ve sınırda onu ikinci bir uygulamaya dönüştürür.
- Durum sapması kapatılamaz: sözleşme testinin birimi tek bir istek ve yanıtıdır, istekler
  arası ilişki bu birimin dışında kalır. Bedel: 2 süreç, 10 istek, on beş satırlık düzenek;
  kazanç süre değil zamanlamadır.

#### Sonraki Adım

Bu konu dört denetim kurdu ve dördü de aynı biçimdeydi: **tek bir sınıra** bakmak. Şema
denetimi bir yanıtı inceledi, sözleşme testi bir tüketici ile bir sağlayıcı arasındaki
alanları saydı, uyumluluk denetimi iki şema metnini karşılaştırdı, sapma tablosu bir sahte
sunucu ile bir gerçek servisi ölçtü. Her biri kendi sınırında güçlüdür ve hepsi birlikte
koştuğunda bile bir soru yanıtsız kalır.

Üyenin raftaki kitabı arayıp ayırttığı ve ödünç aldığı yol, bu denetimlerin hiçbirinin
kapsamında değildir. O yol birden çok isteği, birden çok servisi ve istekler arasında
taşınan durumu birlikte kullanır; son ölçütün kapatılamaz çıkması bunun ilk işaretiydi.
Sonraki konu bu yolu bir bütün olarak koşturan testleri ele alır ve ilk sorusu bir maliyet
sorusudur: aynı hatayı üç ayrı seviyede yakalamak neye mal olur, hangi seviyede kaç test
tutmak gerekir.

## Uçtan Uca ve Arayüz Testleri

Uygulamanın tamamı üzerinden koşan testlerin maliyetinin ve değerinin dengelenmesi: seviyeler arası dağılımın piramitle kurulması, seçici stratejisi ile bekleme yönetiminin kırılganlığa etkisi, aygıt ve emülatör kararları, ekran karşılaştırmasının yararı ile gürültüsü, kırılganlığın nedenlerine göre karantina ve düzeltme disipliniyle yönetilmesi ve koşumun paralelleştirilmesinin süre ile yalıtım arasındaki takası.

### Test Piramidi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/test-piramidi

Aynı ödünç akışının birim, tümleşim ve uçtan uca seviyelerinde koşturulması; her seviyenin yakaladığı ve kaçırdığı kusur sınıfının gösterilmesi ve seviye başına bedelin sorgu, istek ve kurulum sayısıyla ölçülmesi.

Önceki konu dört denetim kurdu: isteğin ve yanıtın şemaya uyduğu, tüketicinin beklentisinin
sağlayıcıda koştuğu, bir şema değişikliğinin kırıcı olup olmadığı ve sahte sunucunun
sözleşmeye bağlı kaldığı sınandı. Hepsinin ortak yanı şudur: her biri **tek bir sınıra**
bakar. Üyenin bir kitabı ödünç alması ise o sınırların hiçbirine sığmaz — kural bir yerde,
kayıt başka bir yerde, arayüzün gördüğü yanıt üçüncüsündedir.

Seviyelerin sınıflandırması Kalite ve Test Temelleri kursunda yapıldı; burada sorulan şey
sınıflandırma değil **dağılımdır**. Kaç test hangi seviyede olmalı sorusunun yanıtı bir
tercih değildir: seviye başına koşum bedeli, yakalanan kusur sınıfı ve aynı kusuru birden
çok seviyede yakalamanın maliyeti ölçülür, dağılım bu üç sayıdan çıkar.

#### Aynı Akış, Üç Seviye

Ödünç alma akışı üç parçaya ayrılmış durumda: uygunluk kuralı saf bir işlev, kayıt gerçek
bir veritabanı, dış yüzey bir HTTP arayüzü. Kural, açık ödünç sayısına bir sınır koyar.

```js
// kural.mjs — surum 1: odunc uygunlugu kurali
export const SINIR = 5;

export function uygunMu({ acikOdunc, gecikmeGunu, uyelik, kitapDurumu }) {
  if (uyelik !== 'etkin') return { uygun: false, sebep: 'uyelik-etkin-degil' };
  if (kitapDurumu !== 'rafta') return { uygun: false, sebep: 'kitap-rafta-degil' };
  if (gecikmeGunu > 0) return { uygun: false, sebep: 'gecikmis-odunc-var' };
  if (acikOdunc > SINIR) return { uygun: false, sebep: 'odunc-siniri-asildi' };
  return { uygun: true, sebep: 'uygun' };
}
```

Depo katmanı kararı gerçek satırlara çevirir. Her sorgu sayılır; bu sayaç dersin bedel
ölçüsüdür.

```js
// depo.mjs — katalog ve odunc kayitlari node:sqlite uzerinde
import { DatabaseSync } from 'node:sqlite';
import { uygunMu } from './kural.mjs';

export const sayac = { sorgu: 0 };

export function depoKur() {
  const db = new DatabaseSync(':memory:');
  db.exec(`CREATE TABLE uye (uye_no TEXT PRIMARY KEY, uyelik TEXT, gecikme_gunu INTEGER);
           CREATE TABLE kitap (kitap_no TEXT PRIMARY KEY, durum TEXT);
           CREATE TABLE odunc (uye_no TEXT, kitap_no TEXT UNIQUE)`);
  return db;
}

export function oduncAl(db, uyeNo, kitapNo) {
  const oku = (s, ...p) => { sayac.sorgu += 1; return db.prepare(s).all(...p); };
  const yaz = (s, ...p) => { sayac.sorgu += 1; db.prepare(s).run(...p); };
  const [uye] = oku('SELECT uyelik, gecikme_gunu FROM uye WHERE uye_no = ?', uyeNo);
  const [kitap] = oku('SELECT durum FROM kitap WHERE kitap_no = ?', kitapNo);
  const [{ n }] = oku('SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?', uyeNo);
  const karar = uygunMu({
    acikOdunc: n, gecikmeGunu: uye.gecikme_gunu, uyelik: uye.uyelik, kitapDurumu: kitap.durum,
  });
  if (karar.uygun === false) return karar;
  yaz('INSERT INTO odunc (uye_no, kitap_no) VALUES (?, ?)', uyeNo, kitapNo);
  yaz("UPDATE kitap SET durum = 'oduncte' WHERE kitap_no = ?", kitapNo);
  return karar;
}
```

Dış yüzeyde iki parça var: kararı bir durum koduna eşleyen küçük işlev ve isteği karşılayan
sunucu.

```js
// durumkodu.mjs — surum 1: karar HTTP durum koduna eslenir
export const durumKodu = (karar) => (karar.uygun ? 201 : 409);
```

```js
// servis.mjs — odunc akisinin HTTP arayuzu
import { createServer } from 'node:http';
import { oduncAl } from './depo.mjs';
import { durumKodu } from './durumkodu.mjs';

export function sunucuKur(db) {
  return createServer((istek, yanit) => {
    let govde = '';
    istek.on('data', (parca) => { govde += parca; });
    istek.on('end', () => {
      const { uyeNo, kitapNo } = JSON.parse(govde);
      const karar = oduncAl(db, uyeNo, kitapNo);
      yanit.writeHead(durumKodu(karar), { 'content-type': 'application/json' });
      yanit.end(JSON.stringify(karar));
    });
  });
}
```

Dört test aynı akışı üç seviyede sınar: biri yalnız kuralı, biri kuralı ve veritabanını,
ikisi de kullanıcının gördüğü yolu.

```js
// seviyeler.test.mjs — ayni odunc akisi uc seviyede sinanir
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { uygunMu } from './kural.mjs';
import { depoKur, oduncAl } from './depo.mjs';
import { sunucuKur } from './servis.mjs';

const doldur = () => {
  const db = depoKur();
  db.prepare("INSERT INTO uye VALUES ('U-17', 'etkin', 0)").run();
  db.prepare("INSERT INTO uye VALUES ('U-31', 'askida', 0)").run();
  db.prepare("INSERT INTO kitap VALUES ('K-90', 'rafta')").run();
  return db;
};

const gonder = async (govde) => {
  const sunucu = sunucuKur(doldur());
  await new Promise((bitir) => sunucu.listen(0, bitir));
  const yanit = await fetch(`http://127.0.0.1:${sunucu.address().port}/odunc`,
    { method: 'POST', body: JSON.stringify(govde) });
  const icerik = await yanit.json();
  sunucu.close();
  return { durum: yanit.status, sebep: icerik.sebep };
};

test('birim: besinci acik odunc siniri doldurur', () => {
  const taban = { gecikmeGunu: 0, uyelik: 'etkin', kitapDurumu: 'rafta' };

  assert.equal(uygunMu({ ...taban, acikOdunc: 4 }).uygun, true);
  assert.equal(uygunMu({ ...taban, acikOdunc: 5 }).sebep, 'odunc-siniri-asildi');
});

test('tumlesim: odunc satiri yazilir ve kitabin durumu doner', () => {
  const db = doldur();

  assert.equal(oduncAl(db, 'U-17', 'K-90').uygun, true);
  const satir = db.prepare('SELECT uye_no, kitap_no FROM odunc').get();
  assert.equal(`${satir.uye_no} ${satir.kitap_no}`, 'U-17 K-90');
  assert.equal(db.prepare("SELECT durum FROM kitap WHERE kitap_no = 'K-90'").get().durum, 'oduncte');
});

test('uctan uca: uygun istek onay dondurur', async () => {
  assert.deepEqual(await gonder({ uyeNo: 'U-17', kitapNo: 'K-90' }), { durum: 201, sebep: 'uygun' });
});

test('uctan uca: askidaki uyelik reddedilir', async () => {
  assert.deepEqual(await gonder({ uyeNo: 'U-31', kitapNo: 'K-90' }),
    { durum: 409, sebep: 'uyelik-etkin-degil' });
});
```

#### Sınır Karşılaştırması Kusuru

Kuralın birinci sürümünde sınır karşılaştırması yanlış yazılmış: sınır beş açık ödünçse,
beşi olan üye altıncıyı alamamalıdır. `acikOdunc > SINIR` yazımı altıncıyı verir ve ancak
yedincide durur.

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

```
not ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 3
# fail 1
```

Birim seviyesi kusuru yakaladı. Aynı koşumda dikkat çeken şey diğer üç satırdır: kusur
koddayken tümleşim ve uçtan uca testleri **yeşil kaldı**. Sebebi basit bir seçimdir — o iki
test mutlu yolu koşuyor, yani sıfır açık ödüncü olan bir üyeyi. Sınır kusurunu üst
seviyede yakalamak için üst seviyede de beş açık ödünçlü bir üye hazırlanmalıydı; bunun
bedeli bir sonraki bölümün konusudur.

Düzeltme tek karşılaştırmadadır.

```js
// kural.mjs — surum 2: sinir karsilastirmasi duzeltildi
export const SINIR = 5;

export function uygunMu({ acikOdunc, gecikmeGunu, uyelik, kitapDurumu }) {
  if (uyelik !== 'etkin') return { uygun: false, sebep: 'uyelik-etkin-degil' };
  if (kitapDurumu !== 'rafta') return { uygun: false, sebep: 'kitap-rafta-degil' };
  if (gecikmeGunu > 0) return { uygun: false, sebep: 'gecikmis-odunc-var' };
  if (acikOdunc >= SINIR) return { uygun: false, sebep: 'odunc-siniri-asildi' };
  return { uygun: true, sebep: 'uygun' };
}
```

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

```
ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 4
# fail 0
```

#### Yalnız Üst Seviyenin Gördüğü Kusur

Ters yönde bir kusur sınıfı da vardır: alt seviyede karşılığı olmayan bir kusur. Reddedilen
bir ödünç isteğinin durum kodu bunlardan biridir — kural için "uygun değil" tek bir değerdir,
depo için kayıt yazmamaktır, ama arayüzü çağıran istemci için bu değer bir durum kodudur.

```js
// durumkodu.mjs — surum 2: red yaniti basari kodu ile donuyor
export const durumKodu = (karar) => (karar.uygun ? 201 : 200);
```

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

```
ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
not ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 3
# fail 1
```

Kural ve depo testleri yeşil; kusur onların gördüğü hiçbir değerde değil. Düzeltme, eşlemeyi
geri almaktır.

```js
// durumkodu.mjs — surum 3: red yaniti catisma kodu ile doner
export const durumKodu = (karar) => (karar.uygun ? 201 : 409);
```

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

```
ok 1 - birim: besinci acik odunc siniri doldurur
ok 2 - tumlesim: odunc satiri yazilir ve kitabin durumu doner
ok 3 - uctan uca: uygun istek onay dondurur
ok 4 - uctan uca: askidaki uyelik reddedilir
# tests 4
# pass 4
# fail 0
```

Eşlemenin kendi işlevine çıkarılmış olması burada rastlantı değildir: `durumKodu` ayrı bir
işlev olduğu için ona bir birim testi yazılabilir ve o kusur bir daha uçtan uca seviyesine
bırakılmaz. Piramit bir sayım kuralı olmadan önce bir tasarım baskısıdır — aşağı
indirilebilen denetim aşağı indirilir.

#### Bedel

Kural dört koşula bakıyor: üyelik, kitap durumu, gecikme ve açık ödünç sayısı. Sınır
davranışını görmek için açık ödünç değerinin üç değeri gerekir (dördü, beşi, altısı); diğer
üç koşulun ikişer değeri vardır. Toplam yirmi dört vaka. Aşağıdaki ölçüm aynı yirmi dört
vakayı üç seviyede koşturur ve seviye başına bedeli sayar.

```js
// bedel.mjs — ayni vaka kumesini uc seviyede kosmanin bedeli sayilir
import { uygunMu } from './kural.mjs';
import { depoKur, oduncAl, sayac } from './depo.mjs';
import { sunucuKur } from './servis.mjs';

const vakalar = [];
for (const uyelik of ['etkin', 'askida']) {
  for (const kitapDurumu of ['rafta', 'oduncte']) {
    for (const gecikmeGunu of [0, 3]) {
      for (const acikOdunc of [4, 5, 6]) vakalar.push({ uyelik, kitapDurumu, gecikmeGunu, acikOdunc });
    }
  }
}

const olcum = { kurulum: 0, istek: 0 };
const hazirla = (vaka) => {
  const db = depoKur();
  db.prepare('INSERT INTO uye VALUES (?, ?, ?)').run('U-17', vaka.uyelik, vaka.gecikmeGunu);
  db.prepare('INSERT INTO kitap VALUES (?, ?)').run('K-90', vaka.kitapDurumu);
  olcum.kurulum += 2;
  for (let i = 0; i < vaka.acikOdunc; i += 1) {
    db.prepare('INSERT INTO odunc VALUES (?, ?)').run('U-17', `E-${i}`);
    olcum.kurulum += 1;
  }
  return db;
};

const s = (n, g) => String(n).padStart(g);
const olc = async (ad, bilesen, kosu) => {
  olcum.kurulum = 0;
  olcum.istek = 0;
  sayac.sorgu = 0;
  const basla = performance.now();
  await kosu();
  const sure = performance.now() - basla;
  console.log(`${ad.padEnd(10)}${s(bilesen, 7)}${s(vakalar.length, 6)}${s(olcum.kurulum, 9)}`
    + `${s(sayac.sorgu, 7)}${s(olcum.istek, 7)}`);
  return sure;
};

console.log(`${'seviye'.padEnd(10)}${s('bilesen', 7)}${s('vaka', 6)}${s('kurulum', 9)}${s('sorgu', 7)}${s('istek', 7)}`);

const birim = await olc('birim', 1, () => {
  for (const vaka of vakalar) uygunMu(vaka);
});

const tumlesim = await olc('tumlesim', 2, () => {
  for (const vaka of vakalar) oduncAl(hazirla(vaka), 'U-17', 'K-90');
});

const uctanUca = await olc('uctan uca', 3, async () => {
  for (const vaka of vakalar) {
    const sunucu = sunucuKur(hazirla(vaka));
    await new Promise((bitir) => sunucu.listen(0, bitir));
    await fetch(`http://127.0.0.1:${sunucu.address().port}/odunc`,
      { method: 'POST', body: JSON.stringify({ uyeNo: 'U-17', kitapNo: 'K-90' }) });
    olcum.istek += 1;
    sunucu.close();
  }
});

const kat = (a, b, esik) => (a / b > esik ? 'evet' : 'hayir');
console.log(`tumlesim suresi birimin 10 katindan uzun mu: ${kat(tumlesim, birim, 10)}`);
console.log(`uctan uca suresi birimin 100 katindan uzun mu: ${kat(uctanUca, birim, 100)}`);
```

```
seviye    bilesen  vaka  kurulum  sorgu  istek
birim           1    24        0      0      0
tumlesim        2    24      168     74      0
uctan uca       3    24      168     74     24
tumlesim suresi birimin 10 katindan uzun mu: evet
uctan uca suresi birimin 100 katindan uzun mu: evet
```

Süre ölçümü makineye ve yüke bağlıdır; bu yüzden çıktı ham milisaniye değil, oran eşiği
basar. Bu koşumda yirmi dört vaka birim seviyesinde 0,02 ms, tümleşim seviyesinde 1,8 ms,
uçtan uca seviyesinde 27,8 ms sürdü; başka bir makinede üç sayı da değişir, sıralamaları
değişmez. Koşumdan bağımsız nicelikler tablodadır: aynı yirmi dört vaka birim seviyesinde
hiçbir sorgu ve istek üretmezken, üst iki seviyede 168 kurulum satırı ve 74 akış sorgusu
üretiyor, en üstte bunlara 24 ağ isteği ekleniyor. Bileşen sütunu da bir bedeldir: bir
seviye kaç parçanın aynı anda ayakta olmasını gerektiriyorsa, o kadar arıza kaynağı
taşır.

Aynı iddianın üç seviyede yinelenmesi bu bedelleri toplar. Yirmi dört vaka her seviyede
tekrarlansa yetmiş iki test olur; kural bir gün değiştiğinde üç ayrı dosya güncellenir ve
üç koşum yeniden yeşile getirilir. Yinelemenin karşılığında yeni bir kusur sınıfı
yakalanmaz: üst seviyeler aynı kararı, üstüne bir taşıma katmanı bindirerek okur.

Piramidin biçimi bu iki gözlemin toplamıdır. Alt seviyede bir vaka bedava sayılır, bu
yüzden vaka kümesi orada tüketilir. Üst seviyede her vaka kurulum, sorgu ve istek satın
alır; bu yüzden orada yalnızca **alt seviyede karşılığı olmayan** kusur sınıfları sınanır —
parçaların birbirine bağlanması, taşıma katmanının eşlemesi, kullanıcının gördüğü yolun
bütünlüğü. Tersi biçim, yani vaka kümesinin üstte tüketilmesi, aynı kusurları çok daha
pahalıya ve çok daha yavaş yakalar.

#### Özet

- Ödünç akışı üç seviyede koşturuldu: kural saf fonksiyon olarak, kayıt gerçek veritabanıyla,
  dış yüzey HTTP arayüzüyle.
- Sınır karşılaştırması kusurunu yalnız birim seviyesi yakaladı; üst iki seviye mutlu yolu
  koştuğu için yeşil kaldı.
- Durum kodu eşlemesi kusurunu yalnız uçtan uca seviyesi yakaladı; kural ve depo testleri
  o değeri hiç görmüyor.
- Yirmi dört vakanın bedeli seviyeye göre 0 sorgu ve 0 istekten 242 sorgu ve 24 isteğe
  çıktı; süre oranı bu koşumda birim seviyesinin yüz katını aştı.
- Piramit bir sayım kuralı değil, iki ölçünün sonucudur: vaka kümesi bedeli düşük seviyede
  tüketilir, üst seviyeye yalnız aşağıda karşılığı olmayan kusur sınıfları kalır.

#### Sonraki Adım

Buradaki uçtan uca testler HTTP arayüzünü sürdü: bir istek gönderildi, bir durum kodu
okundu. Üyenin gerçekte gördüğü yüzey ise bir sayfadır — üzerinde bir arama kutusu, bir
kitap kartı ve bir düğme vardır. O yüzeyi süren bir test, hangi ögeye dokunacağını bir
**seçiciyle** söylemek ve ögenin hazır olmasını beklemek zorundadır. Sonraki ders bu iki
kararı ölçer: seçici türü değiştiğinde testin kaç yapı değişikliğine dayandığını ve bekleme
politikasının düşme oranını nasıl değiştirdiğini.

### Tarayıcı Otomasyonu

Source: https://academia.sh/tr/kurslar/tumlesim-testi/tarayici-otomasyonu

Sayfayı süren testlerin iki kararı olan seçici seçimi ile bekleme politikasının ölçülmesi; dört seçici türünün yapı değişikliği karşısındaki dayanıklılığı ve sabit uyku, örtük bekleme ile açık beklemenin düşme oranı ve bekleme bedeli.

Önceki dersin uçtan uca testleri HTTP arayüzünü sürdü: bir istek gönderildi, bir durum kodu
okundu. Üyenin gerçekte gördüğü yüzey ise bir sayfadır. Bir sayfayı süren test iki karar
vermek zorundadır ve ikisi de kod yazarken görünmez, koşumda görünür: hangi ögeye
dokunulacağı nasıl söylenir ve o ögenin hazır olması nasıl beklenir.

Bu ders gerçek bir tarayıcı sürmez. Sürülen şey sayfanın küçük bir ağaç modelidir; iki
karar da o model üzerinde gerçekten hesaplanır. Ölçülen şey aracın davranışı değil, iki
kararın kendisidir — hangi seçici türü hangi değişiklikte kırılıyor ve hangi bekleme
politikası düşme oranını ne kadar değiştiriyor.

**Seçici** sözcüğü burada CSS ile Görsel Sunum kursundaki seçiciyle aynı sözcüktür, ama iş
farklıdır: orada bir kurala hangi ögelerin uyacağı, burada bir teste hangi ögeyi süreceği
söylenir.

#### Sayfanın Modeli

Ödünç sayfası bir kart taşır: başlık, kitabın durumu ve ödünç alma düğmesi. Düğmenin üç
ayrı tutamağı vardır — konumu, sınıf adı ve test kimliği (test id).

```js
// sayfa.mjs — odunc sayfasinin kucuk agac modeli
export const oge = (etiket, oznitelik = {}, cocuklar = [], metin = '') =>
  ({ etiket, oznitelik, cocuklar, metin });

export function sayfaKur(durum) {
  const dugme = oge('button', { sinif: 'birincil', kimlik: 'odunc-al' }, [],
    durum.dugmeMetni ?? 'Odunc Al');
  const kart = oge('div', { sinif: 'kart', kimlik: 'kitap-karti' }, [
    oge('h2', { sinif: 'baslik' }, [], 'Kaybolan Kitaplar'),
    oge('span', { sinif: 'durum', kimlik: 'kitap-durumu' }, [], durum.kitap),
    dugme,
  ]);
  return { kok: oge('main', {}, [oge('nav', {}, [], 'Katalog'), kart]), dugme, kart };
}
```

Dört seçici türü aynı düğmeyi dört ayrı yoldan arar: ağaçtaki konumdan, sınıf adından,
görünen metinden ve test kimliğinden. Gezilen düğüm sayısı sayılır; bu, seçicinin koşum
bedelidir.

```js
// secici.mjs — dort secici stratejisi ve gezilen dugum sayaci
export const sayac = { dugum: 0 };

function* gez(dugum) {
  sayac.dugum += 1;
  yield dugum;
  for (const cocuk of dugum.cocuklar) yield* gez(cocuk);
}

const ilk = (kok, olcut) => {
  for (const dugum of gez(kok)) if (olcut(dugum)) return dugum;
  return null;
};

const yol = (kok, indisler) => {
  let dugum = kok;
  for (const i of indisler) {
    sayac.dugum += 1;
    dugum = dugum.cocuklar[i];
    if (dugum === undefined) return null;
  }
  return dugum;
};

export const seciciler = {
  konum: (kok) => yol(kok, [1, 2]),
  sinif: (kok) => ilk(kok, (d) => d.oznitelik.sinif === 'birincil'),
  metin: (kok) => ilk(kok, (d) => d.metin === 'Odunc Al'),
  kimlik: (kok) => ilk(kok, (d) => d.oznitelik.kimlik === 'odunc-al'),
};
```

#### Yapı Değişikliği Karşısında Dayanıklılık

Bir arayüz yaşadıkça değişir ve değişikliklerin çoğu davranışı değiştirmez: kart bir düzen
kapsayıcısına sarılır, sınıf adı yenilenir, düğmenin etiketi düzeltilir, kart içindeki
sıra değişir, ölçüm için bir öznitelik eklenir. Beş değişiklik ayrı ayrı, her seferinde
sıfırdan kurulan sayfaya uygulanır ve her seçicinin bulduğu öge asıl düğmeyle
karşılaştırılır.

```js
// dayaniklilik.mjs — bes yapi degisikligi karsisinda secici turleri
import { oge, sayfaKur } from './sayfa.mjs';
import { seciciler, sayac } from './secici.mjs';

const degisiklikler = {
  sarmalama: (s) => { s.kok.cocuklar[1] = oge('div', { sinif: 'duzen' }, [s.kart]); },
  'sinif adi': (s) => { s.dugme.oznitelik.sinif = 'eylem-birincil'; },
  'etiket metni': (s) => { s.dugme.metin = 'Odunc Alin'; },
  'kart sirasi': (s) => { s.kart.cocuklar = [s.kart.cocuklar[0], s.dugme, s.kart.cocuklar[1]]; },
  'izleme ozniteligi': (s) => { s.dugme.oznitelik.izleme = 'kart-birincil'; },
};

const s = (n, g) => String(n).padStart(g);
console.log(`${'secici'.padEnd(8)}${s('tamam', 7)}${s('bulunamadi', 12)}${s('yanlis oge', 12)}${s('dugum', 8)}`);

for (const [ad, secici] of Object.entries(seciciler)) {
  const sonuc = { tamam: 0, bulunamadi: 0, yanlis: 0 };
  sayac.dugum = 0;
  for (const uygula of Object.values(degisiklikler)) {
    const sayfa = sayfaKur({ kitap: 'rafta' });
    uygula(sayfa);
    const bulunan = secici(sayfa.kok);
    if (bulunan === sayfa.dugme) sonuc.tamam += 1;
    else if (bulunan === null) sonuc.bulunamadi += 1;
    else sonuc.yanlis += 1;
  }
  console.log(`${ad.padEnd(8)}${s(sonuc.tamam, 7)}${s(sonuc.bulunamadi, 12)}${s(sonuc.yanlis, 12)}${s(sayac.dugum, 8)}`);
}
```

```
secici    tamam  bulunamadi  yanlis oge   dugum
konum         3           1           1      10
sinif         4           1           0      30
metin         4           1           0      30
kimlik        5           0           0      30
```

Üç sonuç okunur. Birincisi sıralamadır: konuma bağlı seçici beş değişikliğin ikisinde,
sınıf ve metin seçicileri birinde, test kimliği hiçbirinde kırılmadı. Kimlik seçicisinin
dayanıklılığı bir üstünlük değil bir sözleşmedir — o öznitelik yalnız test için durur ve
tasarım değişiklikleri ona dokunmaz.

İkincisi, kırılmanın iki ayrı türüdür. `bulunamadi`, testin kırmızıya dönmesi ve nedenin
hemen okunmasıdır. `yanlis oge` ise sessizdir: kart içindeki sıra değiştiğinde konuma bağlı
seçici düğme yerine durum ögesini bulur, test ona dokunur ve hiçbir şey olmaz. Bu, kırılgan
testlerin en pahalı biçimidir; testin yeşil kalması da mümkündür, kırmızıya dönüp yanlış
yeri göstermesi de.

Üçüncüsü bedeldir. Konuma bağlı seçici beş aramada yalnız on düğüme dokundu, diğer üçü
otuz düğüme. Ağaç büyüdükçe bu fark büyür — ama otuz düğümlük gezinme, sessizce yanlış
ögeye dokunan bir testin bedeliyle karşılaştırılamaz.

#### Bekleme Politikası

Sayfa hazır olmadan sürülemez, ama "hazır" tek bir an değildir: öge önce ağaca girer,
sonra tıklanabilir hâle gelir. Üç politika bu iki anla üç ayrı ilişki kurar. **Sabit uyku**
(sleep) belirli bir süre bekler ve bir kez dener. **Örtük bekleme** (implicit wait) ögenin
varlığını yoklar, göründüğü anda döner. **Açık bekleme** (explicit wait) beklenen koşulun
kendisini — burada tıklanabilirliği — yoklar.

Aşağıdaki koşum iki bin sayfa yüklemesini kendi tohumlu üretecinden üretir; sayfaların
yüzde ikisi ağır yüklenir, yüzde kırkında ögenin görünmesiyle tıklanabilir olması arasında
bir boşluk vardır.

```js
// bekleme.mjs — uc bekleme politikasinin dusme orani, tohum 20
function uretec(tohum) {
  let durum = (tohum * 2654435761) % 2147483647;
  return () => {
    durum = (durum * 48271) % 2147483647;
    return durum / 2147483647;
  };
}

const ARALIK = 25;
const ZAMAN_ASIMI = 1000;
const SABIT = 250;

const olayUret = (rastgele) => {
  const gorunme = rastgele() < 0.02 ? 900 + rastgele() * 600 : 20 + rastgele() * 180;
  const bosluk = rastgele() < 0.6 ? 0 : 10 + rastgele() * 200;
  return { gorunme, tiklanabilir: gorunme + bosluk };
};

const yokla = (olcut) => {
  let t = 0;
  let yoklama = 0;
  while (t <= ZAMAN_ASIMI) {
    yoklama += 1;
    if (olcut(t)) return { t, yoklama, ulasti: true };
    t += ARALIK;
  }
  return { t, yoklama, ulasti: false };
};

const politikalar = {
  'sabit uyku': (olay) => ({ basari: olay.tiklanabilir <= SABIT, bekleme: SABIT, yoklama: 1 }),
  'ortuk bekleme': (olay) => {
    const s = yokla((t) => olay.gorunme <= t);
    return { basari: s.ulasti && olay.tiklanabilir <= s.t, bekleme: s.t, yoklama: s.yoklama };
  },
  'acik bekleme': (olay) => {
    const s = yokla((t) => olay.tiklanabilir <= t);
    return { basari: s.ulasti, bekleme: s.t, yoklama: s.yoklama };
  },
};

const KOSUM = 2000;
const s = (n, g) => String(n).padStart(g);
console.log(`${'politika'.padEnd(15)}${s('dusme orani', 12)}${s('ort. bekleme', 14)}${s('yoklama', 9)}`);

for (const [ad, politika] of Object.entries(politikalar)) {
  const rastgele = uretec(20);
  let dusen = 0;
  let bekleme = 0;
  let yoklama = 0;
  for (let i = 0; i < KOSUM; i += 1) {
    const sonuc = politika(olayUret(rastgele));
    if (sonuc.basari === false) dusen += 1;
    bekleme += sonuc.bekleme;
    yoklama += sonuc.yoklama;
  }
  console.log(`${ad.padEnd(15)}${s(`%${((dusen / KOSUM) * 100).toFixed(1)}`, 12)}`
    + `${s(`${(bekleme / KOSUM).toFixed(0)} ms`, 14)}${s(yoklama, 9)}`);
}
```

```
politika        dusme orani  ort. bekleme  yoklama
sabit uyku            %16.0        250 ms     2000
ortuk bekleme         %40.9        136 ms    12878
acik bekleme           %1.4        181 ms    16477
```

Örtük beklemenin en düşük ortalama beklemeyi vermesi ve aynı zamanda en yüksek düşme
oranını üretmesi rastlantı değildir: yokladığı koşul yanlış koşuldur. Öge ağaca girdiği
anda döner, oysa test ögenin tıklanabilir olmasını istiyordu; aradaki boşluk her açıldığında
bir düşüş üretir. Sabit uyku ters uçtadır — her koşumda iki yüz elli milisaniye ödeyip
düşüşlerin altıda birini önler; hızlı yüklenen sayfada boşuna, ağır yüklenen sayfada
yetersizdir. Açık bekleme, hızlı sayfada erken döndüğü için ortalamada sabit uykudan ucuza
gelir ve düşme oranını yirmide birine indirir.

Açık beklemenin oranı sıfır değil, yüzde bir virgül dört. Kalan düşüşler zaman aşımını aşan
ağır yüklemelerdir. Bekleme politikası kırılganlığı yönetir, yok etmez; artan yoklama sayısı
da bunun bedelidir.

#### Yakalanan ve Kaçan Kusur

Sayfayı süren bir test hangi kusur sınıfını yakalar? Ödünç alma isteği başarılı olduğu
hâlde kartın durum ögesinin güncellenmemesi bunlardan biridir — sunucu doğru yanıtı verir,
önceki dersin uçtan uca testi yeşil kalır, üye ekranda hâlâ "rafta" görür.

```js
// guncelle.mjs — surum 1: tiklamadan sonraki sayfa durumu
export const tiklaSonrasi = (durum) => ({ ...durum, odunc: true });
```

```js
// akis.test.mjs — odunc akisi sayfa modeli uzerinden surulur
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { sayfaKur } from './sayfa.mjs';
import { seciciler } from './secici.mjs';
import { tiklaSonrasi } from './guncelle.mjs';

const kimlikMetni = (kok, kimlik) => {
  if (kok.oznitelik.kimlik === kimlik) return kok.metin;
  for (const cocuk of kok.cocuklar) {
    const bulunan = kimlikMetni(cocuk, kimlik);
    if (bulunan !== null) return bulunan;
  }
  return null;
};

test('sinif adi degisse de kimlik secicisi dugmeyi bulur', () => {
  const sayfa = sayfaKur({ kitap: 'rafta' });
  sayfa.dugme.oznitelik.sinif = 'eylem-birincil';

  assert.equal(seciciler.kimlik(sayfa.kok), sayfa.dugme);
});

test('odunc alindiktan sonra kart durumu guncellenir', () => {
  const durum = { kitap: 'rafta' };
  const dugme = seciciler.kimlik(sayfaKur(durum).kok);
  assert.equal(dugme.etiket, 'button');

  const sonraki = sayfaKur(tiklaSonrasi(durum));

  assert.equal(kimlikMetni(sonraki.kok, 'kitap-durumu'), 'oduncte');
});

test('etiketi yanlis dugme de bulunur ve tiklanir', () => {
  const sayfa = sayfaKur({ kitap: 'rafta', dugmeMetni: 'Iade Et' });

  const dugme = seciciler.kimlik(sayfa.kok);

  assert.equal(dugme.etiket, 'button');
  assert.equal(dugme.oznitelik.kimlik, 'odunc-al');
});
```

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

```
ok 1 - sinif adi degisse de kimlik secicisi dugmeyi bulur
not ok 2 - odunc alindiktan sonra kart durumu guncellenir
ok 3 - etiketi yanlis dugme de bulunur ve tiklanir
# tests 3
# pass 2
# fail 1
```

Düzeltme, tıklama sonrasındaki durumun kitabın durumunu da taşımasıdır.

```js
// guncelle.mjs — surum 2: kitap durumu da guncelleniyor
export const tiklaSonrasi = (durum) => ({ ...durum, odunc: true, kitap: 'oduncte' });
```

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

```
ok 1 - sinif adi degisse de kimlik secicisi dugmeyi bulur
ok 2 - odunc alindiktan sonra kart durumu guncellenir
ok 3 - etiketi yanlis dugme de bulunur ve tiklanir
# tests 3
# pass 3
# fail 0
```

Üçüncü test kaçan sınıfı gösterir ve iki koşumda da yeşildir: düğmenin etiketi "Iade Et"
olsa bile test onu bulur, ona dokunur ve akış tamamlanır. Test kimliğine bağlanan seçici
etikete hiç bakmaz. Yani seçici dayanıklılığının bedeli, etiket metnindeki kusurun bu
testin kapsamından çıkmasıdır; o denetim ayrıca yazılmadıkça yanlış adlı bir düğme yeşil
koşumla yayına gider.

Bedel tarafında bu üç test bu koşumda 28,5 milisaniye sürdü; süre makineye bağlıdır ve
gerçek bir tarayıcı sürüldüğünde mertebe olarak büyür. Koşumdan bağımsız sayılar ise
ölçümlerde duruyor: sürülen ağaç altı düğümlüdür ve kimlik seçicisi beş aramada otuz düğüm
gezmiştir, açık beklemeli iki bin yüklemelik koşum ise on altı bin dört yüz yetmiş yedi
yoklama yapar.

#### Özet

- Sayfayı süren testin iki kararı vardır: ögeyi hangi tutamaktan bulacağı ve hazır olmasını
  nasıl bekleyeceği.
- Beş yapı değişikliğinde konuma bağlı seçici iki kez, sınıf ve metin seçicileri birer kez
  kırıldı; test kimliği hiç kırılmadı.
- Kırılmanın iki türü ayrı bedellidir: bulunamama testi kırmızıya döndürür, yanlış öge
  eşleşmesi sessizce yanlış yere dokunur.
- İki bin yüklemede sabit uyku yüzde on altı, örtük bekleme yüzde kırk virgül dokuz, açık
  bekleme yüzde bir virgül dört düşme oranı verdi; örtük beklemenin ucuzluğu yanlış koşulu
  yoklamasından gelir.
- Sayfa testi kart güncellenmemesi kusurunu yakaladı, düğme etiketinin yanlışlığını
  kaçırdı; kimliğe bağlanan seçici etikete bakmaz.

#### Sonraki Adım

Buradaki bütün ölçümler tek bir sayfa modeli üzerindeydi: tek ağaç, tek zaman çizelgesi.
Aynı arayüz bir avuç aygıtta, birden çok işletim sistemi sürümünde ve farklı ekran
genişliklerinde çalıştığında ise soru değişir. Artık hangi seçicinin dayanıklı olduğu
değil, testin **hangi aygıtlarda** koşacağı sorulur; çünkü aygıt ve sürüm çarpımı, koşum
sayısını sayfa sayısından çok daha hızlı büyütür. Sonraki ders bu çarpımın büyüme hızını,
seçilen alt kümenin kapsadığı kullanıcı payını ve kapsam dışında kalan kusur riskini ölçer.

### Mobil Otomasyonu

Source: https://academia.sh/tr/kurslar/tumlesim-testi/mobil-otomasyonu

Aygıt ve sürüm eksenlerinin çarpımıyla büyüyen koşum matrisinin ölçülmesi; kullanıcı payına göre ve kapsamaya göre seçilen alt kümelerin kapsadığı pay ile yakaladıkları kusur sınıflarının karşılaştırılması ve emülatörün taşıdığı boyutun sınırı.

Önceki dersin bütün ölçümleri tek bir sayfa modeli üzerindeydi: tek ağaç, tek zaman
çizelgesi. Ödünç akışının mobil arayüzü ise tek bir yerde koşmaz. Aynı ekran bir avuç
aygıt sınıfında ve birden çok işletim sistemi sürümünde açılır ve testin sorusu artık
"hangi seçici dayanıklı" değil, "hangi aygıtlarda koşacağım" olur.

Bu ders gerçek aygıt ya da emülatör kurmaz. Ölçülen şey aygıtın kendisi değil **seçim
kararıdır**: matrisin ne kadar hızlı büyüdüğü, seçilen alt kümenin kaç kullanıcıyı temsil
ettiği ve kapsam dışında kalan kusur sınıfının ne olduğu. Üçü de sayılabilir.

#### Matris ve İki Seçim Ölçütü

Model girdisi açıkça yazılır. **MB1 — altı aygıt sınıfı ve beş işletim sistemi sürümü;
paylar üye oturumlarından ölçülmüş kabul edilir ve her eksende toplamları yüzdür; iki
eksen bağımsız varsayılır.** Bağımsızlık varsayımı gerçek kurulumlarda geçerli değildir
(eski sürümler eski aygıtlarda yoğunlaşır); orada hücre payı doğrudan ölçülür ve aşağıdaki
hesap aynı biçimde çalışır.

```js
// matris.mjs — aygit-surum matrisi ve iki alt kume secimi
export const aygitlar = [
  ['kucuk', 22], ['orta', 26], ['buyuk', 18], ['katlanir', 6], ['tablet', 12], ['eski', 16],
];
export const surumler = [['S-6', 4], ['S-7', 9], ['S-8', 17], ['S-9', 31], ['S-10', 39]];

export const hucreler = aygitlar.flatMap(([aygit, ap]) =>
  surumler.map(([surum, sp]) => ({ aygit, surum, pay: (ap * sp) / 100 })));

export const payaGore = (k) => [...hucreler].sort((a, b) => b.pay - a.pay).slice(0, k);

export function kapsamayaGore(k) {
  const secilen = [];
  const gorulen = { aygit: new Set(), surum: new Set() };
  while (secilen.length < k) {
    const kalan = hucreler.filter((h) => secilen.includes(h) === false);
    const kazanc = (h) => (gorulen.aygit.has(h.aygit) ? 0 : 1) + (gorulen.surum.has(h.surum) ? 0 : 1);
    kalan.sort((a, b) => kazanc(b) - kazanc(a) || b.pay - a.pay);
    const secim = kalan[0];
    gorulen.aygit.add(secim.aygit);
    gorulen.surum.add(secim.surum);
    secilen.push(secim);
  }
  return secilen;
}

export const altKumeler = { paya: payaGore(6), kapsamaya: kapsamayaGore(6) };
```

İki ölçüt farklı şeyi eniyiler. Paya göre seçim en çok kullanıcının bulunduğu hücreleri
alır. Kapsamaya göre seçim her adımda henüz görülmemiş bir aygıt sınıfı ya da sürüm
kazandıran hücreyi tercih eder, eşitlikte paya bakar. Altışar hücre seçilir ve üç kusur
sınıfı için yakalama sayılır: sürüme bağlı kusur alt kümede o sürüm varsa, aygıt sınıfına
bağlı kusur o sınıf varsa, tek hücreye özgü kusur ise yalnız o hücre seçildiyse yakalanır.

```js
// kapsama.mjs — iki secimin kullanici payi ve kusur yakalama orani
import { aygitlar, surumler, hucreler, altKumeler } from './matris.mjs';

const oran = (secilen) => {
  const aygit = new Set(secilen.map((h) => h.aygit));
  const surum = new Set(secilen.map((h) => h.surum));
  return {
    pay: secilen.reduce((t, h) => t + h.pay, 0),
    surumKusuru: `${surum.size}/${surumler.length}`,
    aygitKusuru: `${aygit.size}/${aygitlar.length}`,
    hucreKusuru: `${secilen.length}/${hucreler.length}`,
  };
};

const s = (n, g) => String(n).padStart(g);
console.log(`${'secim'.padEnd(16)}${s('hucre', 6)}${s('kullanici payi', 16)}${s('surum kusuru', 14)}${s('aygit kusuru', 14)}${s('hucre kusuru', 14)}`);
for (const [ad, secilen] of [['paya gore', altKumeler.paya], ['kapsamaya gore', altKumeler.kapsamaya]]) {
  const o = oran(secilen);
  console.log(`${ad.padEnd(16)}${s(secilen.length, 6)}${s(`%${o.pay.toFixed(1)}`, 16)}${s(o.surumKusuru, 14)}${s(o.aygitKusuru, 14)}${s(o.hucreKusuru, 14)}`);
}
console.log(`matris ${aygitlar.length} aygit x ${surumler.length} surum = ${hucreler.length} hucre;`
  + ` bir surum eklenirse ${hucreler.length + aygitlar.length}, bir aygit eklenirse ${hucreler.length + surumler.length}`);
console.log('paya gore secilen:', altKumeler.paya.map((h) => `${h.aygit}/${h.surum}`).join(' '));
console.log('kapsamaya gore secilen:', altKumeler.kapsamaya.map((h) => `${h.aygit}/${h.surum}`).join(' '));
```

```
secim            hucre  kullanici payi  surum kusuru  aygit kusuru  hucre kusuru
paya gore            6           %46.9           2/5           4/6          6/30
kapsamaya gore       6           %24.3           5/5           6/6          6/30
matris 6 aygit x 5 surum = 30 hucre; bir surum eklenirse 36, bir aygit eklenirse 35
paya gore secilen: orta/S-10 kucuk/S-10 orta/S-9 buyuk/S-10 kucuk/S-9 eski/S-10
kapsamaya gore secilen: orta/S-10 kucuk/S-9 buyuk/S-8 eski/S-7 tablet/S-6 katlanir/S-10
```

Sayılar takası açık ediyor. Paya göre seçim kullanıcıların yüzde kırk altı virgül dokuzunu
temsil ediyor, ama beş sürümün yalnız ikisine ve altı aygıt sınıfının dördüne dokunuyor;
seçtiği altı hücrenin dördü aynı sürümde. Kapsamaya göre seçim temsil ettiği payı yarıya
indiriyor, buna karşılık her sürüme ve her aygıt sınıfına bir kez giriyor. Hücreye özgü
kusurda ikisi eşit ve ikisi de zayıftır: otuz hücrenin altısı, yani beşte biri.

Matrisin büyümesi bu kararı her yıl yeniden açar. Yeni bir sürüm çıktığında matris otuzdan
otuz altıya, yeni bir aygıt sınıfı girdiğinde otuz beşe gider; alt küme altı hücrede
kalırsa hücre kapsaması beşte birden altıda bire düşer. Koşum bütçesi doğrusal artarken
matris çarpımsal büyür ve bu yüzden mobil koşumun sorusu hiçbir zaman "hepsini koşalım"
olamaz.

#### Emülatörün Taşıdığı Boyut

Kapsama tablosu bir varsayım taşıyor: seçilen hücrenin gerçekten o aygıt sınıfında koştuğu.
**Emülatör** (emulator) bu varsayımı yalnız yarısında karşılar. Emülatör işletim sistemi
sürümünü gerçekten çalıştırır — sürüme bağlı kusurları taşır. Aygıt sınıfına bağlı olan
şeyi ise taşımaz: dokunma yüzeyinin gerçek boyutu, bellek sınırı, ağ kesintisi, kamera ve
sensör, üreticinin arayüz katmanı.

Bunun aritmetiği doğrudan tabloya işler. Kapsamaya göre seçilen altı hücrenin tamamı
emülatörde koşulursa aygıt sınıfına bağlı kusur yakalaması altıda altıdan sıfıra iner;
sürüme bağlı yakalama beşte beş kalır. İki hücre bir **aygıt havuzunda** (device farm)
gerçek donanıma ayrılırsa aygıt yakalaması altıda ikiye çıkar. Karar bu yüzden "emülatör mü
gerçek aygıt mı" değildir; hangi kusur sınıfının hangi hücrede aranacağıdır.

#### Yakaladığı Kusur: Sürüme Bağlı Ayrıştırma

Ödünç akışının sonunda üyeye bir iade tarihi gösterilir. Tarih metnini üreten taraf
uygulamadır, okuyan taraf ise platformun tarih ayrıştırıcısıdır ve bu ayrıştırıcının
davranışı sürüme göre değişir: eski sürümler yalnız sıralı biçimi kabul eder.

```js
// platform.mjs — tarih ayristirmanin surume gore degisen davranisi
const AYRISTIRICI_GENIS = new Set(['S-9', 'S-10']);

export function tarihAyristir(surum, metin) {
  if (/^\d{4}-\d{2}-\d{2}$/.test(metin)) return metin;
  if (AYRISTIRICI_GENIS.has(surum) && /^\d{2}\.\d{2}\.\d{4}$/.test(metin)) {
    const [gun, ay, yil] = metin.split('.');
    return `${yil}-${ay}-${gun}`;
  }
  return null;
}
```

```js
// iade.mjs — surum 1: iade tarihi yerel bicimde uretiliyor
export function iadeTarihi(oduncTarihi, gun = 14) {
  const t = new Date(`${oduncTarihi}T00:00:00Z`);
  t.setUTCDate(t.getUTCDate() + gun);
  const iki = (n) => String(n).padStart(2, '0');
  return `${iki(t.getUTCDate())}.${iki(t.getUTCMonth() + 1)}.${t.getUTCFullYear()}`;
}
```

Aynı akış iki alt kümede de koşturulur; her hücre için üretilen tarihin o sürümde okunup
okunmadığı sınanır.

```js
// aygit.test.mjs — iade tarihi akisi secilen alt kumelerde kosar
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { altKumeler } from './matris.mjs';
import { tarihAyristir } from './platform.mjs';
import { iadeTarihi } from './iade.mjs';

const kos = (secilen) => {
  for (const hucre of secilen) {
    const metin = iadeTarihi('2026-03-04');
    assert.equal(tarihAyristir(hucre.surum, metin), '2026-03-18',
      `${hucre.aygit}/${hucre.surum} iade tarihini okuyamadi: ${metin}`);
  }
};

test('paya gore secilen alt kumede iade tarihi okunur', () => kos(altKumeler.paya));

test('kapsamaya gore secilen alt kumede iade tarihi okunur', () => kos(altKumeler.kapsamaya));
```

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

```
ok 1 - paya gore secilen alt kumede iade tarihi okunur
not ok 2 - kapsamaya gore secilen alt kumede iade tarihi okunur
# tests 2
# pass 1
# fail 1
```

Kusur birdir, alt küme ikidir ve sonuç ikiye ayrılır. Kapsamaya göre seçilen alt küme
S-6, S-7 ve S-8 hücrelerini taşıdığı için kırmızıya döndü. Paya göre seçilen alt küme
**aynı kusurla yeşil kaldı**: içinde yalnız S-9 ve S-10 var, ikisi de yerel biçimi kabul
ediyor. Kaçan sınıf burada bir test biçiminin değil, bir **seçimin** kaçırdığı sınıftır ve
kaçırılan kullanıcılar da sayılabilir: S-6 ile S-8 arası üç sürüm, sürüm ekseninin yüzde
otuzudur.

Düzeltme, tarih metnini platformdan bağımsız biçimde üretmektir.

```js
// iade.mjs — surum 2: iade tarihi platformdan bagimsiz bicimde uretiliyor
export function iadeTarihi(oduncTarihi, gun = 14) {
  const t = new Date(`${oduncTarihi}T00:00:00Z`);
  t.setUTCDate(t.getUTCDate() + gun);
  return t.toISOString().slice(0, 10);
}
```

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

```
ok 1 - paya gore secilen alt kumede iade tarihi okunur
ok 2 - kapsamaya gore secilen alt kumede iade tarihi okunur
# tests 2
# pass 2
# fail 0
```

İki alt kümenin de kaçırdığı bir sınıf da vardır: yalnız tek bir hücrede görülen kusur —
belirli bir aygıt sınıfının belirli bir sürümdeki arayüz katmanı. Tabloda karşılığı otuzda
altıdır; hücreye özgü kusurların beşte dördü her iki seçimde de kapsam dışındadır ve bu
oran alt küme büyütülmeden düzelmez.

Bedelin koşuma bağlı yanı bu koşumda küçüktür — iki test 31,1 milisaniye sürdü, çünkü
burada koşan şey model. Gerçek koşumda bedeli belirleyen sayı tablodadır: on iki hücre
koşumu, yani iki alt küme için altışar aygıt oturumu. Her oturum bir kurulum, bir uygulama
yüklemesi ve bir yıkım demektir; alt küme otuz hücrenin tamamına çıkarıldığında bu sayı beş
katına çıkar, süre de aynı oranda artar.

#### Özet

- Aygıt ve sürüm eksenleri çarpılır: altı aygıt ve beş sürüm otuz hücre eder, yeni bir
  sürüm altı, yeni bir aygıt sınıfı beş hücre ekler.
- Paya göre seçilen altı hücre kullanıcıların yüzde kırk altı virgül dokuzunu temsil etti
  ama beş sürümün ikisine, altı aygıt sınıfının dördüne dokundu.
- Kapsamaya göre seçilen altı hücre payı yüzde yirmi dört virgül üçe indirdi, buna karşılık
  her sürüme ve her aygıt sınıfına girdi.
- Sürüme bağlı tarih ayrıştırma kusurunu yalnız kapsamaya göre seçilen alt küme yakaladı;
  aynı kusurla paya göre seçilen alt küme yeşil kaldı.
- Emülatör sürüm boyutunu taşır, aygıt sınıfı boyutunu taşımaz; altı hücrenin tamamı
  emülatörde koşulursa aygıt kusuru yakalaması altıda altıdan sıfıra iner.

#### Sonraki Adım

Buraya kadar bütün iddialar bir değer üzerineydi: bir durum kodu, bir metin, bir tarih.
Arayüzün asıl çıktısı ise bir görüntüdür ve onun hakkında "eşit mi" diye sorulduğunda soru
birdenbire zorlaşır — iki ekran görüntüsü hiçbir zaman tam olarak aynı piksellerden
oluşmaz. Sonraki ders bu karşılaştırmayı bir eşiğe bağlar ve eşiği tarayarak ölçer: eşik
düştükçe kaç yanlış alarm üretiliyor, eşik yükseldikçe kaç gerçek gerileme kaçıyor.

### Görsel Doğrulama

Source: https://academia.sh/tr/kurslar/tumlesim-testi/gorsel-dogrulama

Ekran görüntüsünün taban görüntüyle piksel karşılaştırması; eşiğin taranarak yanlış alarm ve kaçırma oranlarının ölçülmesi, değişken bölgenin karşılaştırma dışına alınmasının etkisi ve görsel doğrulamanın göremediği kusur sınıfı.

Buraya kadar bütün iddialar bir değer üzerineydi: bir durum kodu, bir metin, bir tarih.
Arayüzün asıl çıktısı ise bir görüntüdür ve bir düğmenin gölgesinin kaybolması, kontrastın
düşmesi ya da kartın bir piksel kayması hiçbir değer iddiasında görünmez. **Görsel
gerileme** testi bu boşluğu doldurmak ister: ekran görüntüsü, kabul edilmiş bir **taban
görüntüyle** (baseline image) karşılaştırılır.

Soru bir eşitlik sorusu gibi durur ve değildir. İki ekran görüntüsü hiçbir zaman aynı
piksellerden oluşmaz; yazı tipi düzleştirme, alt piksel yerleşimi ve ekranda değişen
bölgeler her koşumda fark üretir. Bu yüzden karşılaştırma bir eşiğe bağlanır ve eşik
seçimi doğrudan iki oranı belirler: **yanlış alarm** (gerileme yokken kırmızıya dönme) ve
**kaçırma** (gerileme varken yeşil kalma).

Bu ders ekran görüntüsü almaz. Karşılaştırma gerçek bir piksel dizisi üzerinde yapılır;
ekran, ödünç kartının küçük bir modelidir.

#### Ekranın Piksel Modeli

Kırk sütun, yirmi satır, tek kanal ton. Kart üç şey çizer: başlık şeridi, kitabın durum
metni ve ödünç düğmesi. Sağ üstte de her koşumda değişen bir bölge vardır — kalan süre
göstergesi.

```js
// goruntu.mjs — kart ekraninin kucuk piksel modeli ve karsilastirma
export const EN = 40;
export const BOY = 20;

const dikdortgen = (p, x0, y0, x1, y1, ton) => {
  for (let y = y0; y <= y1; y += 1) for (let x = x0; x <= x1; x += 1) p[y * EN + x] = ton;
};

export function ciz(d) {
  const p = new Uint8Array(EN * BOY).fill(240);
  dikdortgen(p, 2, 2, 30 + (d.baslikKaymasi ?? 0), 4, 90);
  dikdortgen(p, 2, 7, 1 + (d.durumEni ?? 12), 8, 120);
  if (d.dugmeGorunur !== false) {
    const k = d.kaydirma ?? 0;
    dikdortgen(p, 2 + k, 12, 16 + k, 16, d.dugmeTonu ?? 30);
  }
  if (d.koseYuvarlama) for (const [x, y] of [[2, 12], [16, 12], [2, 16], [16, 16]]) p[y * EN + x] = 240;
  dikdortgen(p, 32, 0, 39, 1, d.saatTonu ?? 40);
  return p;
}

export const dislananBolge = () => {
  const m = new Uint8Array(EN * BOY);
  for (let y = 0; y <= 1; y += 1) for (let x = 32; x < EN; x += 1) m[y * EN + x] = 1;
  return m;
};

export const karsilastir = (a, b, pikselEsigi = 12, dislanan = null) => {
  let farkli = 0;
  let sayilan = 0;
  for (let i = 0; i < a.length; i += 1) {
    if (dislanan && dislanan[i] === 1) continue;
    sayilan += 1;
    if (Math.abs(a[i] - b[i]) > pikselEsigi) farkli += 1;
  }
  return { farkli, oran: (farkli / sayilan) * 100 };
};
```

Karşılaştırmanın iki eşiği vardır ve karıştırılmamalıdır. **Piksel eşiği** tek bir pikselin
ne kadar ton farkının önemsiz sayılacağını söyler; burada on iki. **Oran eşiği** ise kaç
pikselin farklı çıkmasının gerileme sayılacağını söyler ve asıl kararı o verir. Aşağıdaki
tarama piksel eşiğini sabit tutup oran eşiğini tarar.

#### Eşik Taraması

Tarama iki küme üretir. Birinci kümede gerileme yoktur: aynı kabul edilmiş durum, değişen
süre göstergesiyle ve onda bir olasılıkla bir piksel kayan başlıkla yeniden çizilir. İkinci
kümede beş gerçek gerileme vardır. Her eşik için birinci kümenin kaçında alarm verildiği
ve ikinci kümenin kaçının kaçırıldığı sayılır. Sağ üstteki değişken bölge bir kez içeride,
bir kez karşılaştırma dışında bırakılarak iki ölçüm alınır.

```js
// esik.mjs — esik taramasi: yanlis alarm ve kacirma oranlari, tohum 31
import { ciz, karsilastir, dislananBolge } from './goruntu.mjs';

function uretec(tohum) {
  let durum = (tohum * 2654435761) % 2147483647;
  return () => {
    durum = (durum * 48271) % 2147483647;
    return durum / 2147483647;
  };
}

const KABUL = { dugmeGorunur: true, dugmeTonu: 30, durumEni: 12, saatTonu: 40 };
const taban = ciz(KABUL);

const gerilemeler = {
  'dugme yok': { dugmeGorunur: false },
  'dusuk kontrast': { dugmeTonu: 200 },
  'dugme kaydi': { kaydirma: 1 },
  'durum kisaldi': { durumEni: 8 },
  'kose yuvarlandi': { koseYuvarlama: true },
};

const esikler = [0, 0.25, 0.5, 1, 2, 5, 10];
const KOSUM = 200;

const denk = (rastgele) => ciz({
  ...KABUL,
  saatTonu: 60 + Math.floor(rastgele() * 8) * 20,
  baslikKaymasi: rastgele() < 0.1 ? 1 : 0,
});

const olc = (dislanan) => {
  const yanlisAlarm = esikler.map(() => 0);
  const kacirma = esikler.map(() => 0);
  const rastgele = uretec(31);
  for (let i = 0; i < KOSUM; i += 1) {
    const oran = karsilastir(taban, denk(rastgele), 12, dislanan).oran;
    esikler.forEach((e, j) => { if (oran > e) yanlisAlarm[j] += 1; });
  }
  const gerilemeAdlari = Object.keys(gerilemeler);
  for (const ad of gerilemeAdlari) {
    for (let i = 0; i < KOSUM / gerilemeAdlari.length; i += 1) {
      const bozuk = ciz({
        ...KABUL, ...gerilemeler[ad], saatTonu: 60 + Math.floor(rastgele() * 8) * 20,
      });
      const oran = karsilastir(taban, bozuk, 12, dislanan).oran;
      esikler.forEach((e, j) => { if (oran <= e) kacirma[j] += 1; });
    }
  }
  return { yanlisAlarm, kacirma };
};

const dislamasiz = olc(null);
const dislamali = olc(dislananBolge());
const s = (n, g) => String(n).padStart(g);
const yuzde = (n) => `%${((n / KOSUM) * 100).toFixed(1)}`;
console.log(`${'oran esigi'.padEnd(11)}${s('yanlis alarm', 14)}${s('kacirma', 9)}`
  + `${s('dislamali alarm', 17)}${s('dislamali kacirma', 19)}`);
esikler.forEach((e, j) => {
  console.log(`${`%${e.toFixed(2)}`.padEnd(11)}${s(yuzde(dislamasiz.yanlisAlarm[j]), 14)}`
    + `${s(yuzde(dislamasiz.kacirma[j]), 9)}${s(yuzde(dislamali.yanlisAlarm[j]), 17)}`
    + `${s(yuzde(dislamali.kacirma[j]), 19)}`);
});
for (const [ad, fark] of Object.entries(gerilemeler)) {
  const oran = karsilastir(taban, ciz({ ...KABUL, ...fark }), 12, dislananBolge()).oran;
  console.log(`${ad.padEnd(16)} dislamali fark orani %${oran.toFixed(2)}`);
}
```

```
oran esigi   yanlis alarm  kacirma  dislamali alarm  dislamali kacirma
%0.00              %100.0     %0.0             %5.5               %0.0
%0.25              %100.0     %0.0             %5.5               %0.0
%0.50              %100.0     %0.0             %0.0               %0.0
%1.00              %100.0     %0.0             %0.0              %20.0
%2.00                %5.5     %0.0             %0.0              %60.0
%5.00                %0.0    %60.0             %0.0              %60.0
%10.00               %0.0    %60.0             %0.0             %100.0
dugme yok        dislamali fark orani %9.57
dusuk kontrast   dislamali fark orani %9.57
dugme kaydi      dislamali fark orani %1.28
durum kisaldi    dislamali fark orani %1.02
kose yuvarlandi  dislamali fark orani %0.51
```

İlk iki sütun, değişken bölge içerideyken hiçbir eşiğin işe yaramadığını gösteriyor.
Süre göstergesi tek başına on altı piksel, yani ekranın yüzde ikisidir; bu yüzden yüzde
ikinin altındaki her eşik her koşumda alarm verir, üstündeki her eşik ise küçük
gerilemelerin hepsini yutar. Yüzde iki eşiği rastlantısal olarak iyi görünür — kaçırma
sıfır, alarm yüzde beş virgül beş — ama bu, eşiğin gürültünün büyüklüğüne oturtulmasıdır:
gösterge bir hane büyüdüğünde eşik yeniden ayarlanmak zorunda kalır.

Değişken bölge karşılaştırma dışına alındığında tablo okunur hâle geliyor. Yüzde yarım
eşiğinde yanlış alarm sıfıra iniyor ve beş gerilemenin beşi de yakalanıyor. Eşik yükseldikçe
kaçırma sırayla açılıyor: yüzde birde köşe yuvarlaması, yüzde ikide durum metni ve düğme
kayması, yüzde onda düğmenin tümden kaybolması bile. Son satırlar bu sıranın nedenini
veriyor — her gerilemenin ürettiği fark oranı sabittir ve eşik onu geçtiği anda kusur
görünmez olur.

Buradan çıkan kural şudur: eşik, kaçırılabilecek en küçük gerçek gerilemenin altında
tutulur; gürültü o eşiği aşıyorsa eşik yükseltilmez, **gürültünün kaynağı karşılaştırmadan
çıkarılır**. Değişken bölgeleri dışlamak eşiği düşük tutmanın bedelidir ve dışlanan her
bölge, orada olabilecek gerilemenin de görülmemesi demektir.

#### Yakaladığı ve Kaçırdığı Kusur

Görsel doğrulamanın yakaladığı sınıf, hiçbir değer iddiasında görünmeyen kusurdur. Biçem
katmanında düğme tonunun açılması böyledir: ağaç aynıdır, seçici çalışır, akış tamamlanır,
düğme okunamayacak kadar soluktur.

```js
// bicem.mjs — surum 1: dugme tonu acildi
export const bicem = { dugmeTonu: 200 };
```

```js
// gorsel.test.mjs — kart ekrani taban goruntuyle karsilastirilir
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { ciz, karsilastir, dislananBolge } from './goruntu.mjs';
import { bicem } from './bicem.mjs';

const KABUL = { dugmeGorunur: true, dugmeTonu: 30, durumEni: 12, saatTonu: 40 };
const taban = ciz(KABUL);
const dislanan = dislananBolge();
const ESIK = 0.5;

test('kart ekrani taban goruntuye uyar', () => {
  const simdi = ciz({ ...KABUL, dugmeTonu: bicem.dugmeTonu, saatTonu: 120 });

  const { farkli, oran } = karsilastir(taban, simdi, 12, dislanan);

  assert.ok(oran <= ESIK, `${farkli} piksel farkli, oran %${oran.toFixed(2)}`);
});

test('etkisiz dugme goruntude fark uretmez', () => {
  const etkin = ciz({ ...KABUL, dugmeTonu: bicem.dugmeTonu, etkin: true, saatTonu: 120 });
  const etkisiz = ciz({ ...KABUL, dugmeTonu: bicem.dugmeTonu, etkin: false, saatTonu: 200 });

  assert.equal(karsilastir(etkin, etkisiz, 12, dislanan).farkli, 0);
});
```

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

```
not ok 1 - kart ekrani taban goruntuye uyar
ok 2 - etkisiz dugme goruntude fark uretmez
# tests 2
# pass 1
# fail 1
```

İkinci test kaçan sınıfı kurar ve iki koşumda da yeşildir: düğmenin etkin ya da etkisiz
olması aynı pikselleri üretir, çünkü modelde etkisizliğin görsel bir karşılığı yoktur.
Görsel doğrulama **görünenin** doğruluğunu sorar; dokunulduğunda ne olduğunu sormaz. Ödünç
düğmesi tıpatıp doğru görünüp hiçbir istek göndermiyorsa bu test yeşil kalır ve o kusur
ancak önceki derslerin akış testleriyle yakalanır.

Düzeltme, tonun taban görüntüdeki değere döndürülmesidir.

```js
// bicem.mjs — surum 2: dugme tonu taban goruntudeki degere donduruldu
export const bicem = { dugmeTonu: 30 };
```

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

```
ok 1 - kart ekrani taban goruntuye uyar
ok 2 - etkisiz dugme goruntude fark uretmez
# tests 2
# pass 2
# fail 0
```

Bedelin koşuma bağlı yanı bu koşumda küçüktür: iki test 29,1 milisaniye sürdü, eşik taraması
sekiz yüz karşılaştırma yaptı; dışlanan bölge içerideyken her karşılaştırma sekiz yüz,
dışlandığında yedi yüz seksen dört piksel okudu. Koşumdan bağımsız bedel ise taban görüntülerin
sayısıdır. Ödünç akışının dört ekranı, önceki dersin kapsamaya göre seçilmiş altı hücresinde
tutulursa yirmi dört taban görüntü saklanır. Düğmenin tonunu bilerek değiştiren bir tasarım
kararı, bu yirmi dördünün tamamını yeniden onaylamayı gerektirir — görsel doğrulamanın asıl
bedeli koşum değil, bu onay işidir.

#### Özet

- Görsel doğrulama, ekran görüntüsünü kabul edilmiş taban görüntüyle karşılaştırır ve karar
  iki eşiğe bağlanır: piksel ton eşiği ve farklı piksel oranı eşiği.
- Değişken bölge karşılaştırmanın içindeyken yüzde ikinin altındaki her eşik yüzde yüz
  yanlış alarm verdi, üstündeki eşikler küçük gerilemeleri kaçırdı.
- Değişken bölge dışlandığında yüzde yarım eşiği sıfır yanlış alarmla beş gerilemenin
  beşini de yakaladı.
- Eşik yükseldikçe kaçırma, gerilemenin ürettiği fark oranı sırasına göre açılır: yüzde
  birde köşe yuvarlaması, yüzde ikide bir piksellik kayma, yüzde onda düğmenin kaybolması.
- Görsel doğrulama görünenin doğruluğunu sorar; etkisiz bir düğme aynı pikselleri ürettiği
  için bu testten yeşil geçer.

#### Sonraki Adım

Son üç ders aynı gerçeği ayrı yüzlerinden gördü: bekleme politikası düşme oranını
değiştiriyordu, aygıt seçimi kusur kapsamasını, eşik seçimi yanlış alarmı. Üçü de tek bir
sorunun parçalarıdır — uçtan uca testler, alt seviyelerdeki testlerin taşımadığı bir
kararsızlık taşır. Sonraki ders bu kararsızlığı toplu olarak ele alır: bir uçtan uca
takımında düşüşlerin nedenleri hangi oranda dağılıyor, karantina sinyali temizlerken kaçan
hatayı ne kadar artırıyor.

### Kırılganlık Yönetimi

Source: https://academia.sh/tr/kurslar/tumlesim-testi/kirilganlik-yonetimi

Uçtan uca takım ölçeğinde kırılganlığın nedenlerine göre dağılımının ölçülmesi, tek tek küçük düşme oranlarının takım düzeyinde nasıl büyüdüğü ve karantina eşiğinin kırmızı koşum ile kaçan gerçek hata arasında kurduğu takas.

Son üç ders aynı gerçeği ayrı yüzlerinden gördü: bekleme politikası düşme oranını
değiştiriyordu, aygıt seçimi kusur kapsamasını, eşik seçimi yanlış alarmı. Üçü de tek bir
sorunun parçalarıdır — uçtan uca test, alt seviyelerdeki testlerin taşımadığı bir
kararsızlık taşır.

Kırılgan testin tanımı, düşme oranı ve karantina Birim Testi ve Test Odaklı Geliştirme
kursunda kurulmuştu. Burada ölçek değişiyor. Orada tek bir testin düşme oranı ölçülüyordu;
burada kırk testlik bir takımın koşumu ölçülür ve iki yeni soru çıkar: düşüşler hangi
nedene ne oranda dağılıyor ve kırılgan testleri karantinaya almak kaçan hatayı ne kadar
artırıyor.

#### Takım Ölçeğinde Dağılım

Model girdisi açıkça yazılır. **KY1 — kırk uçtan uca test. On ikisi dar zaman aşımlı
beklemeye açık (koşum başına 0,02), altısı önceki testin bıraktığı duruma bağlı (0,01),
sekizi aynı üye ve kitap kaydını paylaşıyor (0,015), beşi ağ üzerinden çok sayıda çağrı
yapıyor (çağrı başına 0,005 takılma olasılığı ve altı çağrı, yani 0,0296), dokuzu kararlı.
Gerçek bir gerileme koşumların dörtte birinde bulunur ve kırk koruma sınıfından birine
düşer.** Nedenlerin oranları mekanizmadan gelir: ağ nedeni çağrı sayısıyla büyür, paylaşılan
veri nedeni aynı kaydı kullanan test sayısıyla.

```js
// takim.mjs — kirk uctan uca testin iki yuz kosumu ve karantina taramasi
function uretec(tohum) {
  let durum = (tohum * 2654435761) % 2147483647;
  return () => {
    durum = (durum * 48271) % 2147483647;
    return durum / 2147483647;
  };
}

const testler = [];
const ekle = (adet, neden, oran) => {
  for (let i = 0; i < adet; i += 1) testler.push({ neden, oran, dusus: 0 });
};
ekle(12, 'bekleme', 0.02);
ekle(6, 'sira', 0.01);
ekle(8, 'paylasilan veri', 0.015);
ekle(5, 'ag', 1 - (1 - 0.005) ** 6);
ekle(9, 'kararli', 0);

const KOSUM = 200;
const rastgele = uretec(47);
for (let k = 0; k < KOSUM; k += 1) {
  for (const t of testler) if (rastgele() < t.oran) t.dusus += 1;
}

const s = (n, g) => String(n).padStart(g);
console.log(`${'neden'.padEnd(17)}${s('test', 6)}${s('dusus', 7)}${s('test basina oran', 18)}`);
for (const neden of ['bekleme', 'sira', 'paylasilan veri', 'ag', 'kararli']) {
  const grup = testler.filter((t) => t.neden === neden);
  const dusus = grup.reduce((a, t) => a + t.dusus, 0);
  const oran = ((dusus / (grup.length * KOSUM)) * 100).toFixed(2);
  console.log(`${neden.padEnd(17)}${s(grup.length, 6)}${s(dusus, 7)}${s(`%${oran}`, 18)}`);
}

const GERCEK_HATA = 0.25;
const senaryo = (esik) => {
  const karantinada = testler.map((t) => (t.dusus / KOSUM) * 100 > esik);
  const rastgele2 = uretec(53);
  let kirmizi = 0;
  let kacan = 0;
  for (let k = 0; k < KOSUM; k += 1) {
    let dusen = false;
    testler.forEach((t, i) => {
      if (karantinada[i] === false && rastgele2() < t.oran) dusen = true;
    });
    if (dusen) kirmizi += 1;
    if (rastgele2() < GERCEK_HATA) {
      const koruyan = Math.floor(rastgele2() * testler.length);
      if (karantinada[koruyan]) kacan += 1;
    }
  }
  return { adet: karantinada.filter(Boolean).length, kirmizi, kacan };
};

console.log(`${'karantina esigi'.padEnd(17)}${s('karantinada', 13)}${s('kirmizi kosum', 15)}${s('kacan gercek hata', 19)}`);
for (const esik of [100, 2.5, 1.5, 0.5]) {
  const r = senaryo(esik);
  const ad = esik === 100 ? 'yok' : `%${esik.toFixed(1)}`;
  console.log(`${ad.padEnd(17)}${s(r.adet, 13)}${s(`%${((r.kirmizi / KOSUM) * 100).toFixed(1)}`, 15)}`
    + `${s(`%${((r.kacan / KOSUM) * 100).toFixed(1)}`, 19)}`);
}
```

```
neden              test  dusus  test basina oran
bekleme              12     37             %1.54
sira                  6     10             %0.83
paylasilan veri       8     32             %2.00
ag                    5     28             %2.80
kararli               9      0             %0.00
karantina esigi    karantinada  kirmizi kosum  kacan gercek hata
yok                          0          %47.5               %0.0
%2.5                         8          %36.0               %4.5
%1.5                        13          %26.5               %9.5
%0.5                        25           %9.0              %16.5
```

Birinci tablonun tek tek satırları zararsız görünür: en kırılgan grup bile yüz koşumda üç
kez düşüyor. İkinci tablonun ilk satırı bu izlenimi bozar — karantina yokken koşumların
yüzde kırk yedi virgül beşi kırılganlık yüzünden kırmızı. Aradaki büyüme çarpımdan gelir:
bir koşumun temiz geçmesi için kırk testin kırkının da geçmesi gerekir ve tek tek geçme
olasılıkları çarpılınca yüzde kırk üç virgül altı yedilik bir kırmızı beklentisi çıkar. İki
yüz koşumda ölçülen yüzde kırk yedi virgül beş bu beklentinin örnekleme dalgalanmasıdır.

Bu, uçtan uca ölçeğin birim ölçekten ayrıldığı yerdir. Yüzde iki düşme oranı tek bir birim
testinde katlanılabilir bir kusurdur; kırk testlik bir uçtan uca takımda ise her ikinci
koşumu kırmızıya çeviren bir sistem özelliğidir. Ve düşüşlerin nedenleri sabit değildir:
ölçümdeki en büyük pay ağ ve paylaşılan veridir, ikisi de tek bir testin içinde
düzeltilemez — biri çağrı sayısıyla, öteki düzeneğin ortaklığıyla ilgilidir.

#### Karantinanın İki Yüzü

İkinci tablonun kalan satırları karantina eşiğini tarar. Eşik düştükçe karantinaya alınan
test sayısı artar, kırmızı koşum oranı düşer ve **kaçan gerçek hata** oranı yükselir.
Yüzde iki buçuk eşiği sekiz testi ana koşumdan çıkarıyor: kırmızı koşum yüzde kırk yedi
virgül beşten yüzde otuz altıya iniyor, buna karşılık koşumların yüzde dört buçuğunda
gerçek bir gerileme kimseye görünmeden geçiyor. Yarım yüzde eşiği takımın dörtte birini
karantinaya alıyor; sinyal neredeyse temiz — kırmızı koşum yüzde dokuz — ama koşumların
yüzde on altı buçuğu bir gerilemeyi taşıyıp yeşil dönüyor.

Karantina bu yüzden bir çözüm değil, bir takastır: gürültüyü kapsama karşılığında satın
alır. Kararın ölçüsü de tabloda görünür — hangi eşik seçilirse seçilsin, kaçan hata oranı
karantinaya alınan testlerin payına yaklaşır.

Bir ayrıntı daha var. Karantina kararı, gerçek düşme oranına değil, iki yüz koşumda
**ölçülen** orana bakar. Ölçülen değerler modeldeki gerçek değerlerden sapıyor: bekleme
grubunun gerçek oranı yüzde iki, ölçülen yüzde bir virgül elli dört. Örneklem küçüldükçe
bu sapma büyür ve karantina listesine kararlı testler girmeye, kırılgan testler girmemeye
başlar.

#### Yeşil Koşum, Duran Kusur

Takasın somut hâli tek bir koşumda görülür. Ödünç akışının dört testi var; ikincisi ödünç
sınırı aşıldığında üyeye uyarı gösterildiğini sınıyor. Akışın birinci sürümü reddedilen
isteğe doğru durum kodunu veriyor, ama uyarıyı taşımıyor.

```js
// akis.mjs — surum 1: odunc isteginin uctan uca gorunen sonucu
export const SINIR = 5;

export function oduncIste(uye) {
  if (uye.acikOdunc >= SINIR) return { durum: 409, uyari: null };
  return { durum: 201, uyari: null };
}

export const kartDurumu = (sonuc) => (sonuc.durum === 201 ? 'oduncte' : 'rafta');
```

```js
// karantina.mjs — surum 1: karantinadaki test adlari
export const karantina = new Set();
```

```js
// takim.test.mjs — uctan uca takimin dort testi, karantina listesine bakar
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncIste, kartDurumu } from './akis.mjs';
import { karantina } from './karantina.mjs';

const sinar = (ad, govde) => test(ad, { skip: karantina.has(ad) }, govde);

sinar('uygun istek onaylanir', () => {
  assert.equal(oduncIste({ acikOdunc: 1 }).durum, 201);
});

sinar('odunc siniri asildiginda uyari gosterilir', () => {
  const sonuc = oduncIste({ acikOdunc: 5 });

  assert.equal(sonuc.durum, 409);
  assert.equal(sonuc.uyari, 'odunc-siniri-asildi');
});

sinar('onaydan sonra kart oduncte gorunur', () => {
  assert.equal(kartDurumu(oduncIste({ acikOdunc: 1 })), 'oduncte');
});

sinar('reddedilen istekte kart rafta kalir', () => {
  assert.equal(kartDurumu(oduncIste({ acikOdunc: 5 })), 'rafta');
});
```

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

```
ok 1 - uygun istek onaylanir
not ok 2 - odunc siniri asildiginda uyari gosterilir
ok 3 - onaydan sonra kart oduncte gorunur
ok 4 - reddedilen istekte kart rafta kalir
# tests 4
# pass 3
# fail 1
# skipped 0
```

İkinci test kusuru yakaladı. Şimdi bu testin ağ üzerinden çok sayıda çağrı yaptığı ve
ölçümde kırılgan çıktığı varsayılsın; kural gereği karantinaya alınır.

```js
// karantina.mjs — surum 2: kirilgan bulunan test karantinaya alindi
export const karantina = new Set(['odunc siniri asildiginda uyari gosterilir']);
```

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

```
ok 1 - uygun istek onaylanir
ok 2 - odunc siniri asildiginda uyari gosterilir # SKIP
ok 3 - onaydan sonra kart oduncte gorunur
ok 4 - reddedilen istekte kart rafta kalir
# tests 4
# pass 3
# fail 0
# skipped 1
```

Kod değişmedi, kusur duruyor, koşum yeşil. Kaçan sınıf budur ve tabloda ölçülen "kaçan
gerçek hata" sütununun tek bir örneğidir: karantinaya alınan her test, koruduğu kusur
sınıfını da beraberinde götürür. Atlanan testin çıktıda görünür kalması bu yüzden önemlidir
— yeşil koşumun kaç testi atladığı okunmazsa, takas görünmez olur.

Disiplinin iki parçası vardır ve ikisi de bu koşumda tamamlanır: **kırılganlığın kaynağı
düzeltilir** (burada ağ çağrılarının sayısı ve zaman aşımı), sonra **test karantinadan
çıkarılır**. Aşağıda ikisi birlikte yapılıyor: akış uyarıyı taşır hâle getiriliyor ve liste
boşaltılıyor.

```js
// akis.mjs — surum 2: reddedilen istek uyariyi da tasiyor
export const SINIR = 5;

export function oduncIste(uye) {
  if (uye.acikOdunc >= SINIR) return { durum: 409, uyari: 'odunc-siniri-asildi' };
  return { durum: 201, uyari: null };
}

export const kartDurumu = (sonuc) => (sonuc.durum === 201 ? 'oduncte' : 'rafta');
```

```js
// karantina.mjs — surum 3: kirilganlik kaynagi giderildi, liste bosaltildi
export const karantina = new Set();
```

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

```
ok 1 - uygun istek onaylanir
ok 2 - odunc siniri asildiginda uyari gosterilir
ok 3 - onaydan sonra kart oduncte gorunur
ok 4 - reddedilen istekte kart rafta kalir
# tests 4
# pass 4
# fail 0
# skipped 0
```

Karantina kaydının bir sahibi, bir gerekçesi ve bir çıkış ölçütü olmadığında kalıcılaşır;
kalıcı karantina, tabloların son satırındaki duruma — temiz sinyal ve yüzde on altı buçuk
kaçan hata — yavaşça yaklaşmaktır.

Bedelin koşuma bağlı yanı bu koşumda küçüktür: dört test 30,7 milisaniye sürdü. Koşumdan
bağımsız sayılar ölçümdedir — kırk test iki yüz koşumda sekiz bin test koşumu üretti ve
her karantina kaydı bir bakım borcu, bir kusur sınıfı kaybı demektir.

#### Özet

- Uçtan uca ölçekte kırılganlık tek testin değil takımın özelliğidir: koşum başına yüzde
  iki dolayında düşen kırk test, koşumların yüzde kırk yedi virgül beşini kırmızıya çevirdi.
- Ölçülen düşüşlerin en büyük payları ağ (%2,80) ve paylaşılan veri (%2,00) nedenlerindeydi;
  ikisi de tek bir testin içinde düzeltilemez.
- Karantina eşiği düştükçe kırmızı koşum azalır ve kaçan gerçek hata artar: yüzde iki buçuk
  eşiğinde %36,0 ve %4,5; yarım yüzde eşiğinde %9,0 ve %16,5.
- Karantina kararı gerçek orana değil ölçülen orana dayanır; iki yüz koşumluk örneklem
  bekleme grubunun gerçek yüzde ikilik oranını yüzde bir virgül elli dört gösterdi.
- Tek koşumluk örnek takası somutlaştırdı: karantinaya alınan test atlandığında kusur
  yerinde dururken koşum yeşil döndü.

#### Sonraki Adım

Buradaki bütün testler tek tek koştu: kırk test sırayla, biri bitince öteki. Karantina
tartışmasının arkasında hep aynı sayı duruyordu — koşum süresi. Kırılgan bir test
karantinaya alınmak yerine yeniden koşturulabilirdi, kaçan hatayı azaltmak için daha çok
vaka eklenebilirdi; ikisi de aynı bütçeye, geçen zamana çarpar. Bu da sorunun kendisini
değiştirir: testlerin sırayla koşması bir zorunluluk mudur, yoksa bir seçim midir? Sıradaki
konu bu soruyu koşum düzeninin kendisi üzerinden ele alır ve süreyi kısaltmanın karşılığında
neyin, hangi yalıtım gereksiniminin ödendiğini ölçer.

### Paralel Çalıştırma

Source: https://academia.sh/tr/kurslar/tumlesim-testi/paralel-calistirma

Uçtan uca takımın işçilere bölünmesiyle kazanılan sürenin ve kaybedilen yalıtımın ölçülmesi; seri payın hızlanmaya koyduğu tavan, paylaşılan düzeneğin ürettiği çakışmalar, yalnız paralelde görünen yarış kusuru ve yalıtımın hem bedeli hem kör noktası.

Önceki dersin bütün testleri tek tek koştu ve karantina tartışmasının arkasında hep aynı
sayı duruyordu: koşum süresi. Buradaki soru artık hangi testin yazılacağı değil, testlerin
sırayla koşmasının bir zorunluluk mu yoksa bir seçim mi olduğudur.

Seçimdir ve bedeli vardır. Bir takımı **işçilere** bölmek süreyi kısaltır; aynı işlem
**test yalıtımını** (test isolation) da zorunlu kılar, çünkü sırayla koşarken zararsız olan
her paylaşım artık aynı anda kullanılır. Test yalıtımı, İleri SQL kursundaki yalıtım
düzeyiyle karıştırılmamalıdır: orada eşzamanlı işlemlerin birbirinin verisini görme
kuralı, burada iki testin birbirinin düzeneğine dokunmaması söz konusudur. Bu ders iki
tarafı da sayar: kazanılan süreyi ve yalıtım için ödenen bedeli.

#### Süre ve Seri Pay

Toplam süre yalnız işin bölünmesiyle değil, bölünemeyen parçayla da belirlenir: takım
kurulumu, derleme ve raporlama her koşumda bir kez ve tek parça çalışır. Bu, Sistem
Tasarımına Giriş kursundaki Yatay ve Dikey Ölçekleme dersinde kurulan **seri paydır**;
aritmetiği burada tekrarlanmaz, yalnız uçtan uca takıma uygulanır.

**PC1 — kırk test: yirmisi üç birim, on ikisi altı birim, sekizi on beş birim; seri pay on
iki birim.** Süreler model birimidir, makineye bağlı değildir. Dağıtım en uzun testten
başlayarak en boş işçiye yerleştirir.

```js
// zamanlama.mjs — kirk testin isci sayisina gore toplam suresi (model birimi)
const sureler = [
  ...Array(20).fill(3), ...Array(12).fill(6), ...Array(8).fill(15),
];
const SERI = 12;

const dagit = (isciSayisi) => {
  const yuk = Array(isciSayisi).fill(0);
  for (const sure of [...sureler].sort((a, b) => b - a)) {
    const enAz = yuk.indexOf(Math.min(...yuk));
    yuk[enAz] += sure;
  }
  return Math.max(...yuk);
};

const toplamIs = sureler.reduce((a, b) => a + b, 0);
const enUzun = Math.max(...sureler);
const tek = SERI + dagit(1);
const s = (n, g) => String(n).padStart(g);
console.log(`${'isci'.padEnd(6)}${s('paralel', 9)}${s('toplam', 8)}${s('hizlanma', 10)}${s('seri pay', 10)}`);
for (const isci of [1, 2, 4, 8, 16, 40]) {
  const paralel = dagit(isci);
  const toplam = SERI + paralel;
  console.log(`${String(isci).padEnd(6)}${s(paralel, 9)}${s(toplam, 8)}`
    + `${s(`${(tek / toplam).toFixed(2)}x`, 10)}${s(`%${((SERI / toplam) * 100).toFixed(1)}`, 10)}`);
}
console.log(`is ${toplamIs} birim, seri ${SERI} birim, en uzun test ${enUzun} birim, taban ${SERI + enUzun} birim`);
```

```
isci    paralel  toplam  hizlanma  seri pay
1           252     264     1.00x      %4.5
2           126     138     1.91x      %8.7
4            63      75     3.52x     %16.0
8            33      45     5.87x     %26.7
16           18      30     8.80x     %40.0
40           15      27     9.78x     %44.4
is 252 birim, seri 12 birim, en uzun test 15 birim, taban 27 birim
```

İlk iki satır beklendiği gibidir: iki işçi süreyi neredeyse yarıya indirir. Sonrası öyle
değil. Sekiz işçide hızlanma beş virgül seksen yedi, kırk işçide dokuz virgül yetmiş sekiz
kalır — test sayısı kadar işçi verilse bile onda birin altına inilemez. İki sınır çalışıyor:
seri pay toplamın yüzde kırk dörde kadar çıkan bir parçasını tutuyor ve hiçbir dağıtım en
uzun testten kısa süremez. Yirmi yedi birimlik taban budur.

Buradan çıkan iki iş vardır ve ikisi de işçi eklemekten önce gelir: seri payı küçültmek ve
en uzun testi bölmek. İşçi sayısını artırmak, bu ikisi yapılmadan, ödenen bedeli
karşılamaz.

#### Paylaşılan Düzenek

Ödenen bedel yalıtımdır. Aşağıdaki koşum dört testi işçilere dağıtır ve her testi
adımlarına ayırıp işçiler arasında sırayla ilerletir; paralel koşumun testleri iç içe
geçirmesi burada gerçekten olur. Her test bir üye kaydı kurar, açık ödünç sayısını okur,
ödünç yazar, sınırın aşılmadığını doğrular ve düzeneğini yıkar.

Üç seçenek argümanla verilir: kaç işçi, ödünç kararının **iki adımda** mı (önce oku, sonra
yaz) yoksa **tek adımda** mı verildiği ve düzeneğin paylaşılıp paylaşılmadığı.

```js
// kosum.mjs — dort testin isci sayisi, servis kipi ve yalitim kipine gore kosumu
import { DatabaseSync } from 'node:sqlite';

const [isciSayisi = '1', servis = 'iki-adim', kip = 'paylasilan'] = process.argv.slice(2);
const ISCI = Number(isciSayisi);
const TEST = 4;
const SINIR = 1;
const olcum = { adim: 0, kurulumSorgusu: 0, cakisma: 0, dusen: [] };

const semaKur = () => {
  const db = new DatabaseSync(':memory:');
  db.exec(`CREATE TABLE uye (uye_no TEXT PRIMARY KEY);
           CREATE TABLE odunc (uye_no TEXT, kitap_no TEXT UNIQUE)`);
  olcum.kurulumSorgusu += 2;
  return db;
};

const veritabanlari = kip === 'ayri-sema'
  ? Array.from({ length: ISCI }, semaKur)
  : Array(ISCI).fill(semaKur());

const say = (db, uyeNo) => db.prepare('SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?').all(uyeNo)[0].n;

const testYap = (no, isci) => {
  const db = veritabanlari[isci];
  const uyeNo = kip === 'paylasilan' ? 'U-17' : `U-${no}`;
  const kitapNo = `K-${no}`;
  const yerel = {};
  const yaz = () => db.prepare('INSERT INTO odunc VALUES (?, ?)').run(uyeNo, kitapNo);
  const adimlar = [
    () => {
      olcum.kurulumSorgusu += 1;
      try { db.prepare('INSERT INTO uye VALUES (?)').run(uyeNo); } catch { olcum.cakisma += 1; }
    },
    ...(servis === 'iki-adim'
      ? [() => { yerel.n = say(db, uyeNo); }, () => { if (yerel.n < SINIR) yaz(); }]
      : [() => { if (say(db, uyeNo) < SINIR) yaz(); }]),
    () => {
      const n = say(db, uyeNo);
      if (n > SINIR) olcum.dusen.push(`test ${no}: acik odunc ${n}, sinir ${SINIR}`);
    },
    () => {
      db.prepare('DELETE FROM odunc WHERE kitap_no = ?').run(kitapNo);
      db.prepare('DELETE FROM uye WHERE uye_no = ?').run(uyeNo);
    },
  ];
  return { adimlar, i: 0 };
};

const kuyruklar = Array.from({ length: ISCI }, () => []);
for (let no = 1; no <= TEST; no += 1) kuyruklar[(no - 1) % ISCI].push(testYap(no, (no - 1) % ISCI));

while (kuyruklar.some((k) => k.length > 0)) {
  for (const kuyruk of kuyruklar) {
    const t = kuyruk[0];
    if (t === undefined) continue;
    t.adimlar[t.i]();
    olcum.adim += 1;
    t.i += 1;
    if (t.i === t.adimlar.length) kuyruk.shift();
  }
}

console.log(`isci ${ISCI}  servis ${servis}  kip ${kip}`);
console.log(`adim ${olcum.adim}  kurulum sorgusu ${olcum.kurulumSorgusu}`
  + `  kurulum cakismasi ${olcum.cakisma}  dusen test ${olcum.dusen.length}/${TEST}`);
for (const satir of olcum.dusen) console.log(satir);
```

Tek işçiyle, yani sırayla, takım temizdir.

```bash
node kosum.mjs 1 iki-adim paylasilan
```

```
isci 1  servis iki-adim  kip paylasilan
adim 20  kurulum sorgusu 6  kurulum cakismasi 0  dusen test 0/4
```

#### Yalnız Paralelde Düşen Test

Aynı kod, aynı veri, iki işçi.

```bash
node kosum.mjs 2 iki-adim paylasilan
```

```
isci 2  servis iki-adim  kip paylasilan
adim 20  kurulum sorgusu 6  kurulum cakismasi 2  dusen test 4/4
test 1: acik odunc 2, sinir 1
test 2: acik odunc 2, sinir 1
test 3: acik odunc 2, sinir 1
test 4: acik odunc 2, sinir 1
```

İki ayrı olay birlikte görünüyor. Birincisi **kurulum çakışmasıdır**: iki test aynı üye
kaydını aynı anda kurmaya çalıştı ve ikisinden biri teklik kısıtına takıldı. Bu, testin
kendi düzeneğine ait bir sorundur.

İkincisi bir ürün kusurudur ve adı **kontrol-sonra-yaz yarışıdır**: ödünç kararı iki adımda
veriliyor — önce açık ödünç sayısı okunuyor, sonra kayıt yazılıyor. İki adım arasında başka
bir işçinin yazması, okunan sayıyı geçersiz kılar. İki test de sıfır okudu, ikisi de yazdı,
üyenin bir olan sınırı ikiye çıktı. Sıralı koşumda bu kusur **hiçbir zaman** görünmez;
önceki koşumun sıfır düşüşü onun kanıtıdır. Paralel çalıştırmanın yakaladığı sınıf budur.

Düzeltme, kararı bölünemez tek bir adımda vermektir.

```bash
node kosum.mjs 2 tek-adim paylasilan
```

```
isci 2  servis tek-adim  kip paylasilan
adim 16  kurulum sorgusu 6  kurulum cakismasi 2  dusen test 0/4
```

Testler yeşile döndü, ama kurulum çakışması iki olarak duruyor — ve bu satır ikinci bir
uyarıdır. Çakışmaya uğrayan iki test kendi üyesini kuramadı; ödünç istekleri sınıra takılıp
sessizce reddedildi ve doğrulama yine de geçti. Yeşil oldukları hâlde sınamak istedikleri
şeyi sınamadılar. Düzeneğin paylaşılması, testi düşürmediğinde bile anlamını boşaltır.

#### Yalıtımın Bedeli ve Kör Noktası

Yalıtımın iki düzeyi vardır: her teste ayrı kayıt kümesi vermek ya da her işçiye ayrı şema
kurmak. İkisi de aynı kusurla, iki adımlı servisle koşuluyor.

```bash
node kosum.mjs 2 iki-adim ayri-kayit
```

```
isci 2  servis iki-adim  kip ayri-kayit
adim 20  kurulum sorgusu 6  kurulum cakismasi 0  dusen test 0/4
```

```bash
node kosum.mjs 2 iki-adim ayri-sema
```

```
isci 2  servis iki-adim  kip ayri-sema
adim 20  kurulum sorgusu 8  kurulum cakismasi 0  dusen test 0/4
```

Bedel iki sayıda görünür. Ayrı kayıt kümesi çakışmayı sıfıra indirir ve hiçbir şey
eklemez; ayrı şema kurulum sorgusunu altıdan sekize çıkarır, çünkü her işçi kendi
tablolarını yaratır. İşçi sayısı arttıkça bu fark doğrusal büyür: şema kurulumu işçi başına
ödenir, kayıt kümesi ise test başına.

Kaçan sınıf da tam burada. İki koşum da yeşil, oysa **kusur yerinde duruyor** — servis hâlâ
iki adımda karar veriyor. Yalıtım, iki testin aynı üyeye aynı anda dokunmasını engellediği
için yarışın koşulunu ortadan kaldırdı. Yalıtım kırılganlığı giderirken, kırılganlığı
üreten paylaşımın kendisine bağlı kusur sınıfını da görünmez kılar. Gerçek kullanıcılar
kayıtları paylaşır; test takımı paylaşmadığı sürece o yarış yalnız üretimde ortaya çıkar.

Bu yüzden yalıtım kararı bir denge kararıdır: koşum düzeninin yalıtımı **tam**, ürünün
eşzamanlılık denetimi ise **ayrıca** sınanmalıdır. Bedelin koşuma bağlı yanı bu koşumda
küçüktür — beş yapılandırmanın her biri yirmi milisaniyenin altında sürdü, çünkü koşan şey
süreç içi bir modeldir. Koşumdan bağımsız sayılar ölçümlerdedir: yirmi adım, altı ile sekiz
arası kurulum sorgusu, iki çakışan kayıt ve yüzde dört buçuktan yüzde kırk dörde çıkan seri
pay.

#### Özet

- İşçi eklemek süreyi seri pay ve en uzun test kadar kısaltır: kırk testte kırk işçi bile
  dokuz virgül yetmiş sekiz kat hızlanma verdi, taban yirmi yedi birimde kaldı.
- Paylaşılan düzenek paralelde iki kurulum çakışması üretti; çakışan test düşmese bile
  sınamak istediği şeyi sınamamış olur.
- Kontrol-sonra-yaz yarışı yalnız paralel koşumda göründü: sıralı koşumda sıfır düşüş,
  iki işçide dört testin dördü de düştü.
- Kararı tek adıma indirmek testleri yeşile döndürdü; yalıtım ise aynı kusur yerinde
  dururken koşumu yeşil yaptı.
- Yalıtımın bedeli ölçülebilir: ayrı kayıt kümesi kurulum sorgusunu artırmadı, ayrı şema
  altıdan sekize çıkardı ve bu fark işçi başına büyür.

#### Kurs Kapanışı

Bu kursun on beş dersi aynı üç soruyu yanıtladı. Tablodaki her satır bir test biçiminin ne
gördüğünü, ne göremediğini ve ne kadara mal olduğunu söyler.

| Ders | Yakaladığı hata sınıfı | Yakalamadığı | Bedeli |
|---|---|---|---|
| Tümleşim Testinin Kapsamı | şema uyuşmazlığı | muhatap kimliği uyuşmazlığı | tur başına 4 sorgu, 9 satır düzenek |
| Test Ortamı Yönetimi | bilinmeyen muhatabın reddi | süreç ömrüne bağlı durum | 1 süreç, 6 adım, koşum başına 1 dosya |
| Test Verisi Yönetimi | dağılıma bağlı rapor kusuru | içeriğe bağlı dışa aktarma kusuru | 772 yükleme sorgusu, 78 kilobayt |
| Veritabanı Testleri | geri göçte veri kaybı | geri doldurma değeri uyuşmazlığı | 13 göç adımı, 21 sorgu, 1 dosya |
| Dış Servis Taklidi | istek biçimi kayması | dondurulmuş yanıtın eskimesi | oynatmada 0 süreç, 0 ağ çağrısı |
| API Testi | yapısal uyuşmazlık | anlamsal uyuşmazlık | 2 süreç, sınama başına en az 1 istek, 16 kural |
| Tüketici Güdümlü Sözleşme Testi | sözleşmedeki alanın kaybolması | çalıştırılmamış dalın okuduğu alan | 2 bileşen, 100 istek, 235 baytlık dosya |
| Şema Uyumluluk Denetimi | kırıcı değişiklik | biçimi bozmadan anlamı değiştiren değişiklik | 0 süreç, 0 istek, 9 satırlık sınıf tablosu |
| Sahte Sunucularla Geliştirme | biçim sapması | durum sapması | 2 süreç, 10 istek, 15 satır düzenek |
| Test Piramidi | sınır karşılaştırması, birim seviyesinde | durum kodu eşlemesi, alt seviyelerde | 24 vaka için 242 sorgu ve 24 istek |
| Tarayıcı Otomasyonu | kart durumunun güncellenmemesi | düğme etiketinin yanlışlığı | 2000 yüklemede 16.477 yoklama |
| Mobil Otomasyonu | sürüme bağlı tarih ayrıştırma | hücreye özgü kusurun 24/30'u | 12 hücre koşumu, 30 hücrelik matris |
| Görsel Doğrulama | düğme tonunun açılması | etkisiz düğme, aynı pikselleri üretir | 24 taban görüntü |
| Kırılganlık Yönetimi | reddedilen istekte uyarının yokluğu | karantinadaki testin koruduğu sınıf | 40 test × 200 koşum, 8000 test koşumu |
| Paralel Çalıştırma | kontrol-sonra-yaz yarışı | yalıtımın gizlediği paylaşım kusuru | kurulum sorgusu 6'dan 8'e, seri pay %4,5'ten %44,4'e |

Orta sütun kursun kuralıdır: bir test katmanının değeri ancak **sınırıyla** birlikte
anlatılabilir. On beş satırın hiçbirinde "daha güvenli" bir seçenek yoktur; her satırda
yakalanan bir sınıf, kaçan bir sınıf ve ödenen bir bedel vardır. Bir test biçimini
savunmanın tek yolu bu üçlüyü yazmaktır ve bir biçimi eklemenin gerekçesi ancak
**başka hiçbir katmanda karşılığı olmayan** bir kusur sınıfıdır.

Kursun kendi sınırı da aynı kuralla yazılır. Birinci konu sahte bağımlılıkların gerçeğe
uyup uymadığını sordu, ikincisi sözleşmenin denetlenebilir parçasını ayırdı, üçüncüsü
kullanıcının gördüğü yolu bir bütün olarak koşturdu. Üçünün de sorduğu soru aynıydı:
**işlev doğru mu?** Sistemin yük altında nasıl davrandığı, saldırı altında ne verdiği ve
arıza altında nasıl bozulduğu hiçbir derste sınanmadı. İşlevsel Olmayan Test kursu bu üç
soruyu ele alır.

