İçeriğe geç
academia.sh

Ders 03 / 15

Test Verisi Yönetimi

Üretim benzeri test verisinin hangi özelliğini korumak zorunda olduğunun ölçülmesi: hacmin değil dağılım kuyruğunun yakaladığı rapor kusuru, maskelemenin bozduğu kısıt, dağılım ve ilişki özellikleri ve maskelenen değerin yok ettiği kusur sınıfı.

İçindekiler

Önceki dersin ortamı tek satırlık bir tohum veriyle kurulmuştu: bir üye, bir şube. Testler geçti, çünkü sorulan sorular tek satırla yanıtlanabiliyordu. Üretimdeki katalog böyle değildir, ve fark yalnız hacim değildir.

Bu dersin sorusu şudur: test verisinin hangi özelliği korunmak zorundadır? Yanıt “üretime benzesin” değildir, çünkü bu ölçülemez. Ölçülebilir olan üç özellik vardır — dağılım, kısıt ve ilişki — ve her biri farklı bir kusur sınıfını görünür kılar. Kişisel alanların maskelenmesi bu üç özelliğin hangisini bozuyorsa, o özelliğe bağlı kusur sınıfını da beraberinde götürür.

Veri Üreteci

Üreteç, önceki kurstan gelen doğrusal eşlenik üreteçle tohumdan türetilir; tohum görünür olduğu sürece aynı veri yeniden üretilebilir. kip bağımsız değişkeni yalnız bir şeyi değiştirir: bir üyenin birden çok geciken ödüncü olup olamayacağını.

// uretec.mjs — tohumdan turetilen katalog verisi; kip dagilimin kuyrugunu belirler
export const SEMA = `
CREATE TABLE uye (
  uye_no TEXT PRIMARY KEY, ad TEXT NOT NULL,
  eposta TEXT NOT NULL UNIQUE, sube TEXT NOT NULL);
CREATE TABLE odunc (
  kitap_no TEXT PRIMARY KEY, uye_no TEXT NOT NULL REFERENCES uye(uye_no),
  alis_gunu INTEGER NOT NULL, son_gun INTEGER NOT NULL, iade_gunu INTEGER);`;

export function uretecKur(tohum) {
  let durum = tohum;
  return () => { durum = (durum * 1103515245 + 12345) % 2147483648; return durum / 2147483648; };
}

const ADLAR = ['Ayse', 'Mehmet', 'Zeynep', 'Emre', 'Elif', 'Kerem', 'Deniz', 'Sinem'];
const SOYADLAR = ['Yilmaz, Kara', 'Demir', 'Sahin', 'Koc', 'Aydin'];
const SUBELER = ['merkez', 'kampus', 'sanat'];

export function veriUret(tohum, uyeSayisi, kip = 'kuyruklu') {
  const rast = uretecKur(tohum);
  const uyeler = [];
  const oduncler = [];
  let kitapNo = 0;
  for (let i = 0; i < uyeSayisi; i += 1) {
    const no = `U-${String(i).padStart(4, '0')}`;
    uyeler.push({
      no,
      ad: `${ADLAR[Math.floor(rast() * ADLAR.length)]} ${SOYADLAR[Math.floor(rast() * SOYADLAR.length)]}`,
      eposta: `uye${i}@ornek.gecersiz`,
      sube: SUBELER[Math.floor(rast() * SUBELER.length)],
    });
    const kuyrukta = rast() > 0.85;
    const adet = kuyrukta ? 6 + Math.floor(rast() * 5) : 1 + Math.floor(rast() * 2);
    let gecikmis = 0;
    for (let k = 0; k < adet; k += 1) {
      const alis = 900 + Math.floor(rast() * 80);
      // duz kipte her uyenin en cok bir geciken odunci olur
      const gecikti = rast() > 0.55 && (kip === 'kuyruklu' || gecikmis === 0);
      if (gecikti) gecikmis += 1;
      oduncler.push({
        kitapNo: `K-${String(kitapNo += 1).padStart(5, '0')}`,
        uyeNo: no,
        alisGunu: alis,
        sonGun: alis + 14,
        iadeGunu: gecikti ? null : alis + 5,
      });
    }
  }
  return { uyeler, oduncler };
}

Sınanan iş, geciken ödüncü olan üyeleri sayan rapordur. Kusurlu sürüm üyeleri değil ödünç satırlarını sayar. Beklenen sonuç, sorgudan bağımsız olarak üretilen veriden hesaplanır.

// yukle.mjs — uretilen veriyi node:sqlite'a yukler ve geciken uye raporunu iki surumde sorar
import { DatabaseSync } from 'node:sqlite';
import { SEMA, veriUret } from './uretec.mjs';

export const BUGUN = 1014;

export function veritabaniKur({ uyeler, oduncler }, yol = ':memory:') {
  const db = new DatabaseSync(yol);
  db.exec('PRAGMA foreign_keys = ON;');
  db.exec(SEMA);
  const uyeEkle = db.prepare('INSERT INTO uye VALUES (?, ?, ?, ?)');
  const oduncEkle = db.prepare('INSERT INTO odunc VALUES (?, ?, ?, ?, ?)');
  let sorgu = 0;
  for (const u of uyeler) { uyeEkle.run(u.no, u.ad, u.eposta, u.sube); sorgu += 1; }
  for (const o of oduncler) {
    oduncEkle.run(o.kitapNo, o.uyeNo, o.alisGunu, o.sonGun, o.iadeGunu);
    sorgu += 1;
  }
  return { db, sorgu };
}

const KUSURLU = `SELECT COUNT(*) AS uye_sayisi FROM uye u
  JOIN odunc o ON o.uye_no = u.uye_no
  WHERE o.iade_gunu IS NULL AND o.son_gun < ?`;
const DUZELTILMIS = KUSURLU.replace('COUNT(*)', 'COUNT(DISTINCT u.uye_no)');

export function gecikenUyeSayisi(db, bugun, surum) {
  return db.prepare(surum === 'duzeltilmis' ? DUZELTILMIS : KUSURLU).get(bugun).uye_sayisi;
}

// test kahini: sayim SQL'den bagimsiz olarak uretilen veriden hesaplanir
export function beklenenGecikenUye({ oduncler }, bugun) {
  const kume = new Set();
  for (const o of oduncler) if (o.iadeGunu === null && o.sonGun < bugun) kume.add(o.uyeNo);
  return kume.size;
}

export const VERI_KUMELERI = {
  minimal: () => ({
    uyeler: [{ no: 'U-0000', ad: 'Ayse Demir', eposta: '[email protected]', sube: 'merkez' }],
    oduncler: [{ kitapNo: 'K-00001', uyeNo: 'U-0000', alisGunu: 900, sonGun: 914, iadeGunu: null }],
  }),
  duz: () => veriUret(20260731, 200, 'duz'),
  kuyruklu: () => veriUret(20260731, 200, 'kuyruklu'),
};

Hacim Değil, Kuyruk

İki üretilmiş kümenin hacmi birebir aynıdır. Ayrıldıkları tek yer, üye başına düşen geciken ödünç sayısının dağılımıdır.

// dagilim.mjs — ayni hacimde iki kume: biri kuyruklu, digeri duz
import { VERI_KUMELERI, BUGUN } from './yukle.mjs';

for (const ad of ['duz', 'kuyruklu']) {
  const veri = VERI_KUMELERI[ad]();
  const sayac = new Map();
  for (const o of veri.oduncler) {
    if (o.iadeGunu === null && o.sonGun < BUGUN) sayac.set(o.uyeNo, (sayac.get(o.uyeNo) ?? 0) + 1);
  }
  const gecikenOdunc = [...sayac.values()].reduce((a, b) => a + b, 0);
  const enCok = Math.max(...sayac.values());
  console.log(`${ad.padEnd(8)}: ${veri.uyeler.length} uye, ${veri.oduncler.length} odunc, ` +
    `${sayac.size} geciken uye, ${gecikenOdunc} geciken odunc, uye basina en cok ${enCok}`);
}
duz     : 200 uye, 572 odunc, 143 geciken uye, 143 geciken odunc, uye basina en cok 1
kuyruklu: 200 uye, 572 odunc, 143 geciken uye, 297 geciken odunc, uye basina en cok 8

Aynı test üç kümede koşturulur.

// rapor.test.mjs — ayni rapor testi uc veri kumesinde; VERI ve RAPOR degiskenleriyle secilir
import test from 'node:test';
import assert from 'node:assert/strict';
import { veritabaniKur, gecikenUyeSayisi, beklenenGecikenUye, VERI_KUMELERI, BUGUN } from './yukle.mjs';

const veriAdi = process.env.VERI ?? 'minimal';
const surum = process.env.RAPOR ?? 'kusurlu';

test(`geciken uye sayisi dogru (${veriAdi})`, () => {
  const veri = VERI_KUMELERI[veriAdi]();
  const { db } = veritabaniKur(veri);
  assert.equal(gecikenUyeSayisi(db, BUGUN, surum), beklenenGecikenUye(veri, BUGUN));
});
for v in minimal duz kuyruklu; do
  VERI=$v RAPOR=kusurlu node --test --test-reporter=tap rapor.test.mjs | grep -E '^ *(ok|not ok)'
done
VERI=kuyruklu RAPOR=duzeltilmis node --test --test-reporter=tap rapor.test.mjs | grep -E '^ *(ok|not ok)'
ok 1 - geciken uye sayisi dogru (minimal)
ok 1 - geciken uye sayisi dogru (duz)
not ok 1 - geciken uye sayisi dogru (kuyruklu)
ok 1 - geciken uye sayisi dogru (kuyruklu)

İkinci satır bu dersin ana ölçümüdür. Düz küme, minimal kümenin iki yüz katı büyüklüğünde olduğu hâlde kusuru göremedi; yakalayan şey hacim değil, üye başına birden çok geciken ödüncün bulunmasıydı. Yakalanan kusur sınıfının adı dağılıma bağlı kusurdur ve korunması gereken özellik, dağılımın kuyruğudur. Düzeltme COUNT(*) yerine COUNT(DISTINCT u.uye_no) yazmaktır; dördüncü satır düzeltilmiş sürümün aynı kümede geçtiğini gösteriyor.

Maskelemenin Bozduğu Özellik

Üretim verisi teste taşınırken kişisel alanlar maskelenir. Maskeleme biçimi, hangi özelliğin ayakta kaldığını belirler. Dört biçim aynı kaynak küme üzerinde denenir.

// maskeleme.mjs — dort maskeleme bicimi ve her birinin bozdugu ozellik
import { veritabaniKur, VERI_KUMELERI } from './yukle.mjs';

const kaynak = VERI_KUMELERI.kuyruklu();

const BICIMLER = {
  'sabit eposta  ': (v) => ({ ...v, uyeler: v.uyeler.map((u) => ({ ...u, eposta: '[email protected]' })) }),
  'sabit sube    ': (v) => ({ ...v, uyeler: v.uyeler.map((u) => ({ ...u, sube: 'gizli' })) }),
  'yeni uye no   ': (v) => ({ ...v, uyeler: v.uyeler.map((u) => ({ ...u, no: `M-${u.no.slice(2)}` })) }),
  'turetilmis    ': (v) => ({
    ...v,
    uyeler: v.uyeler.map((u) => ({
      ...u, ad: `Uye-${u.no.slice(2)}`, eposta: `u${u.no.slice(2)}@ornek.gecersiz`,
    })),
  }),
};

const subeSayisi = (db) => db.prepare('SELECT COUNT(DISTINCT sube) AS n FROM uye').get().n;

console.log(`kaynak         : yuklendi, ${subeSayisi(veritabaniKur(kaynak).db)} ayri sube`);
for (const [ad, maske] of Object.entries(BICIMLER)) {
  try {
    const { db } = veritabaniKur(maske(kaynak));
    console.log(`${ad} : yuklendi, ${subeSayisi(db)} ayri sube`);
  } catch (hata) {
    console.log(`${ad} : yuklenemedi, ${hata.message}`);
  }
}
kaynak         : yuklendi, 3 ayri sube
sabit eposta   : yuklenemedi, UNIQUE constraint failed: uye.eposta
sabit sube     : yuklendi, 1 ayri sube
yeni uye no    : yuklenemedi, FOREIGN KEY constraint failed
turetilmis     : yuklendi, 3 ayri sube

Üç bozulma üç ayrı özelliğe karşılık gelir. Sabit e-posta maskesi kısıtı bozar: teklik kısıtı, veri tabanına yüklenmeyi bile engelliyor. Sabit şube maskesi dağılımı bozar: veri yükleniyor, ama şube kırılımlı her rapor artık tek şube görüyor ve şubeye bağlı hiçbir kusur sınıfı sınanamıyor. Üye numarasının yalnız bir tabloda değiştirilmesi ilişkiyi bozar: yabancı anahtar kısıtı yüklemeyi durduruyor, kısıt kapalı olsaydı birleştirme sessizce boş dönerdi. Türetilmiş maske üçünü de korur, çünkü maskelenen değer kaynak değerin tekliğinden türetiliyor ve tabloların tamamına aynı biçimde uygulanıyor.

Bu Verinin Görmediği

Türetilmiş maske üç özelliği de korudu, ama maskelenen alanın içeriğini korumadı. Rapor dışa aktarılırken alanlar virgülle birleştiriliyor ve tırnaklanmıyor.

// disaaktar.mjs — geciken uye raporunu virgulle ayrilmis satirlara cevirir; alanlar tirnaklanmaz
import { veritabaniKur, VERI_KUMELERI, BUGUN } from './yukle.mjs';

const RAPOR = `SELECT u.uye_no, u.ad, u.sube, COUNT(*) AS gecikme FROM uye u
  JOIN odunc o ON o.uye_no = u.uye_no
  WHERE o.iade_gunu IS NULL AND o.son_gun < ?
  GROUP BY u.uye_no, u.ad, u.sube ORDER BY u.uye_no`;

export function disaAktar(satirlar) {
  return satirlar.map((s) => [s.uye_no, s.ad, s.sube, s.gecikme].join(','));
}

export function raporSatirlari(bicim) {
  const kaynak = VERI_KUMELERI.kuyruklu();
  const veri = bicim === 'maskeli'
    ? { ...kaynak, uyeler: kaynak.uyeler.map((u) => ({ ...u, ad: `Uye-${u.no.slice(2)}` })) }
    : kaynak;
  const { db } = veritabaniKur(veri);
  return db.prepare(RAPOR).all(BUGUN);
}
// disaaktar.test.mjs — disa aktarilan her satir dort alan tasimali; veri bicimi degiskenle secilir
import test from 'node:test';
import assert from 'node:assert/strict';
import { disaAktar, raporSatirlari } from './disaaktar.mjs';

const bicim = process.env.VERI_BICIMI ?? 'maskeli';

test(`disa aktarilan satirlar dort alan tasir (${bicim})`, () => {
  const satirlar = disaAktar(raporSatirlari(bicim));
  const bozuk = satirlar.filter((s) => s.split(',').length !== 4);
  assert.deepEqual(bozuk.slice(0, 1), []);
});
VERI_BICIMI=maskeli node --test --test-reporter=tap disaaktar.test.mjs | grep -E '^ *(ok|not ok)'
VERI_BICIMI=kaynak node --test --test-reporter=tap disaaktar.test.mjs | grep -E '^ *(ok|not ok)'
node -e "const m = await import('./disaaktar.mjs'); console.log(m.disaAktar(m.raporSatirlari('kaynak')).find((s) => s.split(',').length !== 4));" --input-type=module
ok 1 - disa aktarilan satirlar dort alan tasir (maskeli)
not ok 1 - disa aktarilan satirlar dort alan tasir (kaynak)
U-0004,Elif Yilmaz, Kara,sanat,1

Maskelenmiş veriyle test yeşil; aynı test kaynak veriyle kırmızı. Kaçan kusur sınıfının adı içeriğe bağlı kusurdur: kusuru tetikleyen şey ad alanının içindeki virgüldür ve türetilmiş maske o virgülü yok etti. Bu, maskelemenin kaçınılmaz bedelidir — kişisel alanın içeriği testte tutulamaz. Karşılığı, içerik biçimini ayrı bir kural listesiyle sınamaktır: maskelenmemiş, üretilmiş ve ayrılıcı içeren adlar dışa aktarma testine el ile eklenir.

Bedel

// bedel.mjs — uretim benzeri verinin bedeli: satir, sorgu, dosya boyutu ve goreli sure
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { veritabaniKur, VERI_KUMELERI } from './yukle.mjs';

const kok = mkdtempSync(join(tmpdir(), 'veri-'));
const ozet = (veri) => createHash('sha256').update(JSON.stringify(veri)).digest('hex').slice(0, 12);

function olc(ad) {
  const veri = VERI_KUMELERI[ad]();
  const yol = join(kok, `${ad}.db`);
  const baslangic = performance.now();
  const { sorgu } = veritabaniKur(veri, yol);
  const sure = performance.now() - baslangic;
  return { satir: veri.uyeler.length + veri.oduncler.length, sorgu, sure, boyut: statSync(yol).size, ozet: ozet(veri) };
}

const minimal = olc('minimal');
const kuyruklu = olc('kuyruklu');
rmSync(kok, { recursive: true, force: true });

console.log(`minimal  : ${String(minimal.satir).padStart(3)} satir, ${String(minimal.sorgu).padStart(3)} yukleme sorgusu, ${minimal.boyut} bayt`);
console.log(`kuyruklu : ${kuyruklu.satir} satir, ${kuyruklu.sorgu} yukleme sorgusu, ${kuyruklu.boyut} bayt`);
console.log(`tohum 20260731 ayni ozeti veriyor : ${ozet(VERI_KUMELERI.kuyruklu()) === kuyruklu.ozet}`);
console.log(`kuyruklu kume daha uzun yuklendi  : ${kuyruklu.sure > minimal.sure}`);
minimal  :   2 satir,   2 yukleme sorgusu, 24576 bayt
kuyruklu : 772 satir, 772 yukleme sorgusu, 77824 bayt
tohum 20260731 ayni ozeti veriyor : true
kuyruklu kume daha uzun yuklendi  : true

Yedi yüz yetmiş iki yükleme sorgusu ve yetmiş sekiz kilobaytlık bir dosya, her koşumda yeniden ödenir. Bu bedelin karşılığı tek bir kusur sınıfıdır: dağılıma bağlı kusur. Aynı bedeli düz kümeyle ödemek o sınıfı kazandırmadı; hacim tek başına bir ölçüt değildir. Tohumun görünür olması bedeli sabitler — dosyada saklanan bir veri kümesi yerine on iki haneli bir sayı taşınır ve aynı veri yeniden üretilir.

Özet

  • Test verisinin üç ölçülebilir özelliği vardır: dağılım, kısıt ve ilişki; her biri ayrı bir kusur sınıfını görünür kılar.
  • Aynı hacimdeki iki kümeden yalnız kuyruklu olan rapor kusurunu yakaladı; iki yüz katı büyüklükteki düz küme kusuru göremedi.
  • Sabit değerli maske teklik kısıtını, sabit alan maskesi dağılımı, tek tabloda yapılan yeniden numaralandırma ilişkiyi bozar; türetilmiş maske üçünü de korur.
  • Türetilmiş maske alanın içeriğini koruyamaz: ad içindeki ayrılıcıya bağlı dışa aktarma kusuru maskelenmiş veriyle yeşil, kaynak veriyle kırmızı kaldı.
  • Bedel yedi yüz yetmiş iki yükleme sorgusu ve yetmiş sekiz kilobayttır; tohum görünür olduğu sürece veri kümesi taşınmaz, yeniden üretilir.

Sonraki Adım

Buradaki bütün ölçümler tek bir şemayı varsaydı: tablolar hazır, sütunlar yerinde, kısıtlar tanımlı. Şema kendi başına bir ürün parçasıdır ve zaman içinde değişir — sütun eklenir, sütun bölünür, kısıt sıkılaştırılır. Bu değişiklikler göç adı verilen sıralı adımlarla uygulanır ve her adımın bir ileri, bir de geri yönü vardır. Sonraki ders göçün kendisini teste alır: adımların sırası bozulduğunda ne olur, geri göç veriyi koruyabiliyor mu ve şemanın beklenen biçimde olduğu nasıl doğrulanır.

İ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