İçeriğe geç
academia.sh

Ders 02 / 15

Test Ortamı Yönetimi

Bağımlı servisin testten önce ayağa kaldırılması: kurulum ve yıkım adımlarının sayılması, hazır olma yoklamasının bir adım olarak kurulması, önceki dersin kaçan kusur sınıfının gerçek sınırda yakalanması ve süreç ömrüne bağlı kusurun neden kaçtığının gösterilmesi.

İçindekiler

Önceki derste katalog sınırı gerçek çalıştı, çünkü kataloğun gerçeği aynı süreçte yaşayabiliyordu. Bildirim kanalının gerçeği yaşayamaz: ayrı bir süreçtir, bir bağlantı noktası dinler ve kendi verisini okur. Bu, testin çevresine bir test ortamı (test environment) koymayı gerektirir — testten önce kurulan, testten sonra yıkılan ve her koşumda bedeli yeniden ödenen bir düzenek.

Bu ders o düzeneği adım adım kurar ve adımları sayar. Kurulan ortam, önceki dersin kaçan kusur sınıfını — muhatap kimliği uyuşmazlığını — yakalar. Kaçırdığı sınıf da aynı ölçüde belirgindir ve gösterilir.

Bağımlı Servis

Bildirim kanalı, muhatabı üye tablosunda arayan küçük bir servistir. Tanımadığı bir muhatap için 404 döndürür, tanıdığı için 202 ile kuyruğa alır. Sağlık ucu, ortamın hazır olup olmadığını sorabilmesi içindir.

// bildirim-servisi.mjs — bagimli servis: uye tablosunu okur, bilinmeyen muhataba 404 doner
import { createServer } from 'node:http';
import { DatabaseSync } from 'node:sqlite';

export function servisKur(bagl, katalogYolu) {
  const db = new DatabaseSync(katalogYolu);
  const kuyruk = [];
  const sunucu = createServer((istek, yanit) => {
    if (istek.url === '/saglik') {
      yanit.writeHead(200, { 'content-type': 'application/json' });
      return yanit.end('{"durum":"hazir"}');
    }
    if (istek.method === 'GET' && istek.url === '/bildirim') {
      yanit.writeHead(200, { 'content-type': 'application/json' });
      return yanit.end(JSON.stringify({ sayi: kuyruk.length }));
    }
    let govde = '';
    istek.on('data', (p) => { govde += p; });
    istek.on('end', () => {
      const { muhatap, metin } = JSON.parse(govde);
      const uye = db.prepare('SELECT uye_no FROM uye WHERE uye_no = ?').get(muhatap);
      if (uye === undefined) {
        yanit.writeHead(404, { 'content-type': 'application/json' });
        return yanit.end(`{"hata":"bilinmeyen muhatap: ${muhatap}"}`);
      }
      kuyruk.push({ muhatap, metin });
      yanit.writeHead(202, { 'content-type': 'application/json' });
      yanit.end('{"durum":"kuyruga alindi"}');
    });
  });
  sunucu.listen(bagl, '127.0.0.1');
  return sunucu;
}

if (process.argv[2] !== undefined) servisKur(Number(process.argv[2]), process.argv[3]);

Ortamın Adımları

Ortam dört adımdan oluşur ve adımların sırası bağlayıcıdır: geçici dizin açılır, şema ile tohum verisi yazılır, süreç başlatılır, hazır olma yoklaması yapılır. Yıkım iki adımdır.

// ortam.mjs — test ortami: gecici dizin, sema, bagimli surec ve hazir olma yoklamasi
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';

export const ADIMLAR = ['gecici dizin', 'sema ve tohum verisi', 'surec baslatma', 'hazir olma yoklamasi'];
export const SEMA = `
CREATE TABLE uye (uye_no TEXT PRIMARY KEY, sube TEXT NOT NULL);
CREATE TABLE odunc (
  kitap_no TEXT PRIMARY KEY, uye_no TEXT NOT NULL, sube TEXT NOT NULL,
  alis_gunu INTEGER NOT NULL, son_gun INTEGER NOT NULL);
INSERT INTO uye VALUES ('U-17', 'merkez');`;

async function hazirBekle(taban, enCok = 100) {
  for (let deneme = 1; deneme <= enCok; deneme += 1) {
    try {
      const y = await fetch(`${taban}/saglik`);
      if (y.ok) { await y.text(); return deneme; }
    } catch { /* surec henuz dinlemiyor */ }
    await new Promise((c) => setTimeout(c, 20));
  }
  throw new Error('bagimli servis hazir olmadi');
}

export async function ortamKur(bagl) {
  const kok = mkdtempSync(join(tmpdir(), 'ortam-'));
  const katalogYolu = join(kok, 'katalog.db');
  const db = new DatabaseSync(katalogYolu);
  db.exec(SEMA);
  db.close();
  const taban = `http://127.0.0.1:${bagl}`;
  const baslat = () => spawn(process.execPath, ['bildirim-servisi.mjs', String(bagl), katalogYolu],
    { stdio: 'ignore' });
  let surec = baslat();
  await hazirBekle(taban);
  return {
    taban, katalogYolu, surecSayisi: 1,
    yenidenBaslat: async () => {
      surec.kill('SIGTERM');
      await new Promise((c) => setTimeout(c, 200));
      surec = baslat();
      await hazirBekle(taban);
    },
    kapat: () => { surec.kill('SIGTERM'); rmSync(kok, { recursive: true, force: true }); },
  };
}

Dördüncü adım gereksiz görünebilir: süreç başlatıldıysa hazırdır. Değildir. Süreç başlatmak, sürecin bağlantı noktasını dinlemeye başlaması demek değildir; ikisinin arasında yorumlayıcının açılması, modüllerin yüklenmesi ve veri tabanının açılması vardır. Yoklama atlandığında ne olduğu ölçülebilir.

// hazirsiz.mjs — hazir olma yoklamasi atlandiginda surec baslatma ile ilk istek yarisir
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { SEMA } from './ortam.mjs';

const kok = mkdtempSync(join(tmpdir(), 'hazirsiz-'));
const yol = join(kok, 'katalog.db');
const db = new DatabaseSync(yol);
db.exec(SEMA);
db.close();

const surec = spawn(process.execPath, ['bildirim-servisi.mjs', '8934', yol], { stdio: 'ignore' });
try {
  await fetch('http://127.0.0.1:8934/saglik');
  console.log('yoklamasiz ilk istek: yanit alindi');
} catch (hata) {
  console.log(`yoklamasiz ilk istek: ${hata.cause?.code ?? hata.code}`);
}
surec.kill('SIGTERM');
rmSync(kok, { recursive: true, force: true });
yoklamasiz ilk istek: ECONNREFUSED

Bu, ortam yönetiminin kendi kusur sınıfıdır ve adı hazır olma yarışıdır. Yoklama yerine sabit bir bekleme konması sorunu çözmez, yalnız gizler: bekleme kısa kaldığında test düşer, uzun tutulduğunda her koşum o süreyi öder. Yoklama, süreyi ortamın kendisine sordurur.

Kaçan Sınıfın Yakalanması

Bildirim artık bir casus nesne değil, gerçek bir istemcidir.

// istemci.mjs — bildirim kanalinin gercek gerceklestirimi: yapilandirilan adrese gider
export function bildirimIstemcisi(taban) {
  return {
    gonder: async (muhatap, metin) => {
      const yanit = await fetch(`${taban}/bildirim`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ muhatap, metin }),
      });
      if (yanit.status !== 202) {
        const govde = await yanit.json();
        throw new Error(`bildirim reddedildi: ${yanit.status} ${govde.hata}`);
      }
    },
  };
}

Ödünç servisinin iki sürümü sınanır: önceki dersin muhataba üye türü yazan kusurlu sürümü ile üye numarası yazan düzeltilmiş sürümü. Bildirim çağrısı süreç dışına çıktığı için oduncVer artık eşzamansızdır; bu, sınırın kendisinden gelen bir sonuçtur.

// servisler.mjs — odunc servisinin iki surumu: muhataba uye numarasi ya da uye turu yazan
import { DatabaseSync } from 'node:sqlite';

export const KURALLAR = { ogrenci: { oduncGun: 28 }, uye: { oduncGun: 14 } };

export function dosyaKatalogu(yol) {
  const db = new DatabaseSync(yol);
  return {
    ekle: (k) => db.prepare('INSERT INTO odunc VALUES (?, ?, ?, ?, ?)')
      .run(k.kitapNo, k.uyeNo, k.sube, k.alisGunu, k.sonGun),
    bul: (kitapNo) => db.prepare('SELECT uye_no FROM odunc WHERE kitap_no = ?').get(kitapNo),
  };
}

function servisKur({ katalog, bildirim, saat }, muhatabiSec) {
  return {
    async oduncVer(uye, kitapNo) {
      const bugun = saat();
      const kayit = {
        uyeNo: uye.no, kitapNo, sube: uye.sube,
        alisGunu: bugun, sonGun: bugun + KURALLAR[uye.tur].oduncGun,
      };
      katalog.ekle(kayit);
      await bildirim.gonder(muhatabiSec(uye), `son tarih: ${kayit.sonGun}`);
      return kayit;
    },
  };
}

export const kusurluServis = (b) => servisKur(b, (uye) => uye.tur);
export const duzeltilmisServis = (b) => servisKur(b, (uye) => uye.no);

Test, ortamı kurulum ve yıkım kancalarına bağlar. Ortam bir kez kurulur, tek test onu kullanır ve yıkım her durumda çalışır.

// ortam.test.mjs — bildirim siniri gercek calisiyor; SERVIS degiskeni hangi surumun sinandigini secer
import test, { before, after } from 'node:test';
import assert from 'node:assert/strict';
import { ortamKur } from './ortam.mjs';
import { bildirimIstemcisi } from './istemci.mjs';
import { dosyaKatalogu, kusurluServis, duzeltilmisServis } from './servisler.mjs';

const UYE = { no: 'U-17', tur: 'uye', sube: 'merkez' };
let ortam;

before(async () => { ortam = await ortamKur(8931); });
after(() => ortam.kapat());

test('odunc verildiginde bildirim kabul edilir', async () => {
  const kur = process.env.SERVIS === 'duzeltilmis' ? duzeltilmisServis : kusurluServis;
  const servis = kur({
    katalog: dosyaKatalogu(ortam.katalogYolu),
    bildirim: bildirimIstemcisi(ortam.taban),
    saat: () => 1000,
  });
  await servis.oduncVer(UYE, 'K-903');
  const kuyruk = await (await fetch(`${ortam.taban}/bildirim`)).json();
  assert.equal(kuyruk.sayi, 1);
});
SERVIS=kusurlu node --test --test-reporter=tap ortam.test.mjs | grep -E '^ *(ok|not ok|error:|# (tests|pass|fail))'
not ok 1 - odunc verildiginde bildirim kabul edilir
  error: 'bildirim reddedildi: 404 bilinmeyen muhatap: uye'
# tests 1
# pass 0
# fail 1

Önceki derste bu kusur yeşil kalmıştı: casus nesne, kendisine verilen her muhatabı kabul ediyordu. Gerçek servis kabul etmiyor ve reddin gerekçesini yazıyor. Düzeltilmiş sürüm aynı ortamda geçiyor.

SERVIS=duzeltilmis node --test --test-reporter=tap ortam.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - odunc verildiginde bildirim kabul edilir
# tests 1
# pass 1
# fail 0

Bu Ortamın Görmediği

Ortam süreci bir kez ayağa kaldırır ve testten sonra indirir. Aradaki tüm koşum, tek bir süreç ömrü içinde geçer. Bu, bir kusur sınıfını dokunulmadan bırakır: kabul edilen bildirimler yalnız bellekte durmaktadır.

// kalicilik.mjs — testin yesil kaldigi durum ile surecin yeniden baslamasindan sonraki durum
import { ortamKur } from './ortam.mjs';

const ortam = await ortamKur(8932);
const gonder = (muhatap) => fetch(`${ortam.taban}/bildirim`, {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ muhatap, metin: 'son tarih: 1014' }),
});
const kuyruk = async () => (await (await fetch(`${ortam.taban}/bildirim`)).json()).sayi;

await gonder('U-17');
console.log(`test yesilken kuyruk               : ${await kuyruk()}`);
await ortam.yenidenBaslat();
console.log(`surec yeniden baslatildiktan sonra : ${await kuyruk()}`);
console.log(`katalog dosyasi ayakta kaldi       : ${ortam.katalogYolu.endsWith('katalog.db')}`);
ortam.kapat();
test yesilken kuyruk               : 1
surec yeniden baslatildiktan sonra : 0
katalog dosyasi ayakta kaldi       : true

Test yeşil, kuyruk uçucu. Kaçan kusur sınıfının adı süreç ömrüne bağlı durumdur: bağımlılığın verisi yeniden başlatmayı geçirmiyor ve tek ömürlü bir ortam bunu hiçbir koşumda sormaz. Bu sınıfın yakalanması, ortama beşinci bir adım — bağımlılığı koşum ortasında yeniden başlatma — eklemeyi gerektirir; o adımın bedeli de her koşumda ödenir.

Bedel

Ortamın bedeli üç koşumdan bağımsız nicelikle ve bir süre karşılaştırmasıyla yazılır.

// bedel.mjs — gercek sinirin bedeli: surec, kurulum adimi ve casus nesneye gore sure
import { ortamKur, ADIMLAR } from './ortam.mjs';
import { bildirimIstemcisi } from './istemci.mjs';

const N = 100;
const ortam = await ortamKur(8933);
const gercek = bildirimIstemcisi(ortam.taban);
const casus = { gonder: async () => {} };

async function olc(bildirim) {
  const baslangic = performance.now();
  for (let i = 0; i < N; i += 1) await bildirim.gonder('U-17', 'son tarih: 1014');
  return performance.now() - baslangic;
}

await olc(gercek);
const casusSure = await olc(casus);
const gercekSure = await olc(gercek);
ortam.kapat();

console.log(`ayaga kalkan surec  : ${ortam.surecSayisi}`);
console.log(`kurulum adimi       : ${ADIMLAR.length} (${ADIMLAR.join(', ')})`);
console.log(`yikim adimi         : 2 (surec sonlandirma, gecici dizin silme)`);
console.log(`kosum basina dosya  : 1 (katalog dosyasi)`);
console.log(`${N} bildirimde gercek sinir daha uzun surdu : ${gercekSure > casusSure}`);
console.log(`sure farki en az yirmi kat                 : ${gercekSure / casusSure >= 20}`);
ayaga kalkan surec  : 1
kurulum adimi       : 4 (gecici dizin, sema ve tohum verisi, surec baslatma, hazir olma yoklamasi)
yikim adimi         : 2 (surec sonlandirma, gecici dizin silme)
kosum basina dosya  : 1 (katalog dosyasi)
100 bildirimde gercek sinir daha uzun surdu : true
sure farki en az yirmi kat                 : true

Bir süreç, dört kurulum adımı, iki yıkım adımı ve koşum başına bir dosya. Bu altı adımın her biri bakım gerektirir: şema değiştiğinde ikinci adım, servisin başlatma arayüzü değiştiğinde üçüncü adım, sağlık ucu değiştiğinde dördüncü adım yeniden yazılır.

Aynı adımları yalıtılmış bir kap (container) içinde yürüten bir araç sınıfı vardır. Kap, adımların sayısını azaltmaz — geçici dizin yerine geçici bir kap, süreç başlatma yerine kap başlatma gelir — ama iki şeyi değiştirir: bağımlılığın sürümü ve yapılandırması koşumdan koşuma aynı kalır, ve bağımlılığın yeniden başlatılması ucuzlar. Yukarıdaki kaçan kusur sınıfı da tam olarak orada yakalanabilir hâle gelir. Karşılığında her koşum bir imaj çekme ya da önbellekten okuma bedeli öder.

Özet

  • Test ortamı, testin çevresinde kurulan ve yıkılan bir düzenektir; burada dört kurulum ve iki yıkım adımından oluşuyor.
  • Hazır olma yoklaması bir adımdır: atlandığında ilk istek bağlantı reddiyle döner ve yerine konan sabit bekleme ya kırılganlık ya da sabit gecikme üretir.
  • Gerçek bildirim sınırı, önceki dersin kaçan kusur sınıfını yakaladı: bilinmeyen muhatap 404 ile reddedildi ve test kırmızıya döndü.
  • Tek ömürlü bir ortam, süreç ömrüne bağlı durumu göremez; kabul edilen bildirimler yeniden başlatmadan sonra yok oldu, test yine de yeşildi.
  • Bedel bir süreç, altı adım ve koşum başına bir dosyadır; kap tabanlı bir araç sınıfı bu adımları ortadan kaldırmaz, sürüm kararlılığı ve ucuz yeniden başlatma karşılığında yer değiştirir.

Sonraki Adım

Bu ortamın şeması ile birlikte yazılan tohum verisi tek satırdı: bir üye, tek şube. Testler geçti, çünkü sorulan sorular tek satırla yanıtlanabiliyordu. Üretimdeki katalog böyle değildir — üye başına düşen ödünç sayısı geniş bir dağılım gösterir, kayıtlar birbirine bağlıdır ve kısıtlar yalnız belirli bileşimlerde ihlal edilir. Sonraki ders test verisinin kendisini ele alır: üretim benzeri veri hangi özelliğini korumak zorundadır, kişisel alanlar maskelendiğinde hangi özellik bozulur ve tek satırlık bir tohumun göremediği kusur sınıfı nedir.

İ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