Ders 09 / 15
Sahte Sunucularla Geliştirme
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ı.
İçindekiler
Ö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.
// 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.
// 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.
// 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.
// 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.
// 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(", ")}`); }); }
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
// 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.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.