---
title: 'Test Verisi Yönetimi'
source: 'https://academia.sh/tr/kurslar/tumlesim-testi/test-verisi-yonetimi'
course: 'Tümleşim, Sözleşme ve Uçtan Uca Test'
language: tr
updated: '2026-08-17T18:10:51+00:00'
license: 'CC BY-SA 4.0'
---

# 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ı.

Ö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ı.

```js
// 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.

```js
// 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: 'uye0@ornek.gecersiz', 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.

```js
// 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.

```js
// 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));
});
```

```sh
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.

```js
// 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: 'gizli@ornek.gecersiz' })) }),
  '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.

```js
// 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);
}
```

```js
// 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), []);
});
```

```sh
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

```js
// 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.
