---
title: 'Tüketici Güdümlü Sözleşme Testi'
source: 'https://academia.sh/tr/kurslar/tumlesim-testi/tuketici-gudumlu-sozlesme-testi'
course: 'Tümleşim, Sözleşme ve Uçtan Uca Test'
language: tr
updated: '2026-08-17T18:10:50+00:00'
license: 'CC BY-SA 4.0'
---

# Tüketici Güdümlü Sözleşme 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.
