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.