İçeriğe geç
academia.sh

Ders 07 / 15

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.

İçindekiler

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

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

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

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

// 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(", ")}`);
  });
}
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.

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

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

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat