---
title: 'Sözleşme Testleri'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/sozlesme-testleri'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:44+00:00'
license: 'CC BY-SA 4.0'
---

# Sözleşme Testleri

Tüketici beklentilerinin dosyaya yazılması, sağlayıcıya karşı çalıştırılması, kırıcı bir değişiklikte hangi tüketicinin sınamasının düştüğünün gösterilmesi ve etki alanının değişiklikten önce hesaplanması.

Makine okunur tanım, sunucunun ne ürettiğini denetliyor ama tüketicinin ne **kullandığını**
bilmiyor. Tanım ödünç kaydının beş alanı olduğunu söyler; raf terminali bunların hepsini mi
okuyor, yoksa yalnız ikisini mi? Bu bilinmeden her değişiklik en kötü duruma göre planlanır
ve hiçbir alan güvenle kaldırılamaz.

Sözleşme testleri bu boşluğu kapatır. Her tüketici, sağlayıcıdan ne beklediğini yazar;
sağlayıcı bu beklentileri kendi sınama takımının parçası olarak çalıştırır. Beklentiler
tüketicinin **gerçek kullanımından** türetildiği için tanımdan farklı bir bilgi taşır: tanım
neyin verildiğini, beklenti neyin kullanıldığını söyler.

## Tüketici Beklentisi

Beklenti dosyası tüketicinin adını, yaptığı istekleri ve o isteklerden okuduğu alanları
içerir. Ödünç servisinin iki tüketicisi vardır: raflardaki barkod terminali ve üyelerin
telefon uygulaması.

```json
{
  "tuketici": "raf-terminali",
  "etkilesimler": [
    {
      "ad": "kaydın kimliği ve durumu okunur",
      "istek": { "yontem": "GET", "yol": "/odunc/O-1" },
      "beklenen": { "kod": 200, "alanlar": { "/id": "string", "/durum": "string" } }
    }
  ]
}
```

```json
{
  "tuketici": "mobil-uygulama",
  "etkilesimler": [
    {
      "ad": "ödünç kaydı ekranda gösterilir",
      "istek": { "yontem": "GET", "yol": "/odunc/O-1" },
      "beklenen": { "kod": 200, "alanlar": { "/uye": "string", "/kalemler/0/isbn": "string", "/iadeTarihi": "string" } }
    },
    {
      "ad": "geçersiz üye kimliği alan hatasıyla reddedilir",
      "istek": { "yontem": "POST", "yol": "/odunc", "govde": { "uye": "1001", "kalemler": [{ "isbn": "978-0262033848" }] } },
      "beklenen": { "kod": 422, "alanlar": { "/type": "string", "/errors/0/yol": "string", "/errors/0/kod": "string" } }
    }
  ]
}
```

Beklentinin ne **içermediği** en az içerdiği kadar önemlidir. Raf terminali `uye` ve
`kalemler` alanlarını hiç yazmamıştır, çünkü okumamaktadır. Mobil uygulama `id` alanını
yazmamıştır. Beklenti dosyası "yanıt şu alanları içerir" demez, "bu tüketici şu alanlara
bağımlıdır" der. Fark, sağlayıcının kaldırdığı bir alanın kimi bozacağını belirler.

Mobil uygulamanın ikinci beklentisi hata yolunu da kapsıyor. Hata sözleşmesi de
sözleşmenin parçasıdır: 422 yanıtının `errors` dizisinde yol ve kod alanlarının bulunması,
o uygulamanın hataları giriş kutularının yanında gösterebilmesinin koşuludur.

## Beklentilerin Sağlayıcıya Karşı Çalıştırılması

Sağlayıcı iki sürümde çalışabilir: v1 ve kırıcı değişikliğin yapıldığı v2. Kırıcı
değişiklik `iadeTarihi` alanının `sonTarih` olması ve `uye` alanının nesneye dönmesidir.

```js
// saglayici.mjs — odunc servisi saglayicisi
// Kullanim: node saglayici.mjs <port> [v2]
//   v2: iadeTarihi alani sonTarih olur, uye nesneye doner (kirici degisiklik)
import { createServer } from "node:http";

const PORT = Number(process.argv[2] ?? 8438);
const V2 = process.argv[3] === "v2";

const KAYIT = { id: "O-1", uye: "U-1001", kalemler: [{ isbn: "978-0262033848" }], iadeTarihi: "2026-03-20", durum: "acik" };

const disaVer = (k) => V2
  ? { id: k.id, uye: { kimlik: k.uye }, kalemler: k.kalemler, sonTarih: k.iadeTarihi, durum: k.durum }
  : { ...k };

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];
  const json = (kod, g, tip = "application/json") => {
    yanit.writeHead(kod, { "content-type": `${tip}; charset=utf-8` });
    yanit.end(JSON.stringify(g));
  };

  if (istek.method === "GET" && yol === "/odunc/O-1") return json(200, disaVer(KAYIT));

  if (istek.method === "POST" && yol === "/odunc") {
    const g = JSON.parse((await govdeOku(istek)) || "{}");
    if (!/^U-\d{4}$/.test(g.uye ?? "")) {
      return json(422, { type: "https://ornek.kutuphane/sorunlar/dogrulama", title: "İstek gövdesi doğrulanamadı",
        status: 422, detail: "Üye kimliği biçimi geçersiz.", instance: "ol-0001",
        errors: [{ yol: "/uye", kod: "bicim" }] }, "application/problem+json");
    }
    return json(201, disaVer({ ...KAYIT, id: "O-2", uye: g.uye, kalemler: g.kalemler ?? [] }));
  }

  json(404, { type: "https://ornek.kutuphane/sorunlar/kaynak-yok", title: "Kaynak bulunamadı",
    status: 404, detail: `${yol} yok.`, instance: "ol-0002" }, "application/problem+json");
}).listen(PORT, "127.0.0.1", () => console.log(`saglayici 127.0.0.1:${PORT} ${V2 ? "v2" : "v1"}`));
```

Sınama dosyası beklentileri dizinden okur, sağlayıcıyı ayrı bir süreç olarak başlatır ve
her etkileşimi gerçekten yollar.

```js
// sozlesme.test.mjs — tuketici beklentilerini saglayiciya karsi calistirir
// Kullanim: node --test sozlesme.test.mjs           (saglayici v1)
//           SAGLAYICI_SURUM=v2 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 { readdirSync, readFileSync } from "node:fs";

const PORT = 8438;
const TABAN = `http://127.0.0.1:${PORT}`;
let surec;

before(async () => {
  surec = spawn("node", ["saglayici.mjs", String(PORT), process.env.SAGLAYICI_SURUM ?? "v1"], { stdio: "ignore" });
  for (let i = 0; i < 40; i++) {                       // saglayici ayaga kalkana kadar bekle
    try { await fetch(`${TABAN}/odunc/O-1`); return; } catch { await new Promise((c) => setTimeout(c, 50)); }
  }
  throw new Error("sağlayıcı başlatılamadı");
});
after(() => surec.kill());

const oku = (govde, yol) => yol.split("/").slice(1).reduce((d, p) => (d == null ? undefined : d[p]), govde);
const tipi = (d) => (Array.isArray(d) ? "array" : d === null ? "null" : typeof d);

for (const dosya of readdirSync("beklentiler").sort()) {
  const { tuketici, etkilesimler } = JSON.parse(readFileSync(`beklentiler/${dosya}`, "utf8"));
  for (const e of etkilesimler) {
    test(`${tuketici}: ${e.ad}`, async () => {
      const cevap = await fetch(TABAN + e.istek.yol, {
        method: e.istek.yontem,
        headers: e.istek.govde ? { "content-type": "application/json" } : {},
        body: e.istek.govde ? JSON.stringify(e.istek.govde) : undefined,
      });
      assert.equal(cevap.status, e.beklenen.kod, "durum kodu");
      const govde = await cevap.json();
      for (const [yol, beklenenTip] of Object.entries(e.beklenen.alanlar)) {
        const deger = oku(govde, yol);
        assert.notEqual(deger, undefined, `${yol} alanı yanıtta yok`);
        assert.equal(tipi(deger), beklenenTip, `${yol} alanının tipi`);
      }
    });
  }
}
```

```bash
node --test sozlesme.test.mjs 2>&1 | head -8
```

```
✔ mobil-uygulama: ödünç kaydı ekranda gösterilir (59.873209ms)
✔ mobil-uygulama: geçersiz üye kimliği alan hatasıyla reddedilir (3.696709ms)
✔ raf-terminali: kaydın kimliği ve durumu okunur (0.830209ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
```

Süreler makineye göre değişir. Şimdi sağlayıcı kırıcı değişikliği yapar:

```bash
SAGLAYICI_SURUM=v2 node --test sozlesme.test.mjs 2>&1 | head -20
```

```
✖ mobil-uygulama: ödünç kaydı ekranda gösterilir (60.630959ms)
✔ mobil-uygulama: geçersiz üye kimliği alan hatasıyla reddedilir (4.205875ms)
✔ raf-terminali: kaydın kimliği ve durumu okunur (2.19525ms)
ℹ tests 3
ℹ suites 0
ℹ pass 2
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 127.002333

✖ failing tests:

test at sozlesme.test.mjs:28:5
✖ mobil-uygulama: ödünç kaydı ekranda gösterilir (60.630959ms)
  AssertionError [ERR_ASSERTION]: /uye alanının tipi
  
  'object' !== 'string'
```

Üç sınamadan biri düştü. Düşen sınama hangi **tüketicinin** hangi **etkileşiminin** hangi
**alanında** bozulduğunu adıyla söylüyor. Raf terminalinin sınaması geçti, çünkü değişen
alanları okumuyor.

Bu, tanıma karşı doğrulamanın veremeyeceği bir bilgidir. Tanım denetimi "yanıt tanımdan
saptı" der ve durur; sözleşme sınaması "mobil uygulamanın ödünç ekranı bozuldu, raf
terminali etkilenmedi" der. İlki bir kural ihlalini, ikincisi bir sonucu bildirir.

## Etkinin Değişiklikten Önce Hesaplanması

Sınamanın düşmesi için değişikliğin yapılmış olması gerekir. Beklenti dosyaları
değişiklikten **önce** de okunabilir: hangi alanın hangi tüketici tarafından okunduğu zaten
yazılıdır.

```js
// etki.mjs — beklenti dosyalarindan, hangi degisikligin hangi tuketiciyi bozacagini cikarir
import { readdirSync, readFileSync } from "node:fs";

// Tuketici -> bagimli oldugu alan yollari
const bagimlilik = new Map();
for (const dosya of readdirSync("beklentiler").sort()) {
  const { tuketici, etkilesimler } = JSON.parse(readFileSync(`beklentiler/${dosya}`, "utf8"));
  const yollar = etkilesimler.flatMap((e) => Object.keys(e.beklenen.alanlar).map((y) => `${e.istek.yol} ${y}`));
  bagimlilik.set(tuketici, new Set(yollar));
}

// Tanimin GET /odunc/O-1 yanitinda bildirdigi butun alanlar
const TANIMLI = ["/id", "/uye", "/kalemler/0/isbn", "/iadeTarihi", "/durum"].map((y) => `/odunc/O-1 ${y}`);

console.log("alan                           bağımlı tüketiciler");
for (const yol of TANIMLI) {
  const bagli = [...bagimlilik].filter(([, k]) => k.has(yol)).map(([t]) => t);
  console.log(`${yol.replace("/odunc/O-1 ", "").padEnd(30)} ${bagli.length ? bagli.join(", ") : "— (kimse okumuyor)"}`);
}

// Onerilen degisiklik: iadeTarihi kalkiyor, uye nesneye donuyor.
const DEGISIKLIK = ["/odunc/O-1 /iadeTarihi", "/odunc/O-1 /uye"];
const etkilenen = [...bagimlilik].filter(([, k]) => DEGISIKLIK.some((d) => k.has(d))).map(([t]) => t);

console.log(`\nönerilen değişiklik: ${DEGISIKLIK.map((d) => d.split(" ")[1]).join(", ")}`);
console.log(`etkilenen tüketici: ${etkilenen.length}/${bagimlilik.size}  (${etkilenen.join(", ")})`);
console.log(`hiç okunmayan alan sayısı: ${TANIMLI.filter((y) => ![...bagimlilik.values()].some((k) => k.has(y))).length}`);
```

```
alan                           bağımlı tüketiciler
/id                            raf-terminali
/uye                           mobil-uygulama
/kalemler/0/isbn               mobil-uygulama
/iadeTarihi                    mobil-uygulama
/durum                         raf-terminali

önerilen değişiklik: /iadeTarihi, /uye
etkilenen tüketici: 1/2  (mobil-uygulama)
hiç okunmayan alan sayısı: 0
```

Bu tablo, kullanımdan kaldırma dersindeki telemetriyle aynı işi alan düzeyinde yapar.
Telemetri hangi tüketicinin hangi **sürümü** çağırdığını sayıyordu; beklenti dosyaları
hangi tüketicinin hangi **alanı** okuduğunu söylüyor. İkisi birlikte, bir değişikliğin
maliyetini tahmin değil hesap konusu yapar.

Son satır ayrıca bir uyarıdır. "Hiç okunmayan alan sayısı: 0" güven verici görünür ama
yalnızca **beklenti dosyası yazmış** tüketiciler için doğrudur. Bir alanın kimse
tarafından okunmadığı sonucu, ancak bütün tüketiciler beklentilerini yazdığında geçerlidir.
Sözleşme sınamalarının en kırılgan yeri budur: eksik beklenti, olmayan bağımlılık gibi
görünür.

Bu yüzden sözleşme sınaması iki tarafın da katıldığı bir düzendir. Tüketici beklentisini
yazar ve sağlayıcının deposuna verir; sağlayıcı bu dosyaları kendi sınama takımında
çalıştırır. Beklenti yazmayan tüketici, sağlayıcının kararlarında görünmez ve kırıldığında
haber vermekten başka bir gücü kalmaz. Kütüphanenin kendi raf terminali için bu düzenin
kurulması kolaydır; dışarıdaki ilçe kütüphanelerinin uygulamaları için beklenti toplamak,
düzenin en zor parçasıdır.

## Özet

- Makine okunur tanım neyin verildiğini, tüketici beklentisi neyin kullanıldığını söyler;
  bir alanın kaldırılmasının kimi bozacağı ancak ikincisinden bilinir.
- Beklenti dosyasının içermediği alanlar, tüketicinin o alanlara bağımlı olmadığı anlamına
  gelir; hata yanıtının yapısı da beklentinin parçasıdır.
- Beklentiler sağlayıcıya karşı çalıştırıldığında kırıcı değişiklik, hangi tüketicinin
  hangi etkileşiminin hangi alanında bozulduğunu adıyla bildirir.
- Tanım denetimi bir kural ihlalini, sözleşme sınaması bir sonucu bildirir; ikisi farklı
  sorulara yanıt verir.
- Beklenti dosyaları değişiklikten önce de okunabilir ve önerilen bir değişikliğin
  etkilediği tüketici sayısı hesaplanabilir.
- "Kimsenin okumadığı alan" sonucu yalnızca beklentisini yazmış tüketiciler için geçerlidir;
  eksik beklenti, olmayan bağımlılık gibi görünür.

## Sonraki Adım

Beklenti dosyaları şu ana kadar tek yönlü kullanıldı: tüketici yazdı, sağlayıcı sınadı. Aynı
dosyalar ters yönde de işe yarar. Yeni bir tüketici geliştirilmeye başlandığında sağlayıcı
henüz o uç noktayı yazmamış olabilir; tüketicinin beklemesi ise gereksizdir, çünkü
sözleşme zaten bellidir. Sonraki ders şemadan örnek yanıt üreten bir sunucu yazar,
istemciyi ona karşı geliştirir ve aynı istemci kodunun gerçek sunucuya geçtiğinde
değişmeden çalıştığını gösterir.
