Ders 06 / 15
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.
İçindekiler
Ö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.
// 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.
// 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.
// 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ı.
// 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.
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.
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.
// 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.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.