İçeriğe geç
academia.sh

Ders 04 / 15

Veritabanı Testleri

Göç adımlarının ve şemanın teste alınması: ileri göçün ürettiği şeklin doğrulanması, göç numarasının tekilliği, geri göçte veri kaybının yakalanması, göçün var olan veri üzerinde kısıt ihlaliyle düşmesi ve şema testinin göremediği geri doldurma değeri.

İçindekiler

Önceki derste şema sabit bir varsayımdı: tablolar hazır, sütunlar yerinde, kısıtlar tanımlı. Şema kendi başına bir ürün parçasıdır ve sıralı adımlarla değişir. Bu adımların her birinin bir ileri, bir de geri yönü vardır ve ikisi de kod kadar kırılabilir.

Bu ders veri tabanını öğretmez; göçü ve şemayı teste alır. Sorular şunlardır: ileri göç beklenen şekli üretiyor mu, adımların numaralandırması sağlam mı, geri göç veriyi koruyor mu ve göç var olan veri üzerinde ne yapıyor. Bütün ölçümler gerçek bir node:sqlite veritabanı üzerinde koşar.

Göç Kümesi

Dört adım vardır. Üçüncü adımın geri yönü iki sürümde verilir; sınanacak olan budur.

// gocler.mjs — sirali goc adimlari; ucuncu adimin geri yonu iki surumde verilir
export const GERI_SURUMU = process.env.GERI_GOC ?? 'kusurlu';

export const GOCLER = [
  {
    no: 1,
    ad: 'baslangic semasi',
    ileri: [
      `CREATE TABLE uye (uye_no TEXT PRIMARY KEY, ad TEXT NOT NULL, sube TEXT NOT NULL)`,
      `CREATE TABLE odunc (kitap_no TEXT PRIMARY KEY, uye_no TEXT NOT NULL,
         alis_gunu INTEGER NOT NULL, son_gun INTEGER NOT NULL)`,
    ],
    geri: [`DROP TABLE odunc`, `DROP TABLE uye`],
  },
  {
    no: 2,
    ad: 'odunce sube sutunu',
    ileri: [
      `ALTER TABLE odunc ADD COLUMN sube TEXT`,
      `UPDATE odunc SET sube = 'merkez'`,
    ],
    geri: [`ALTER TABLE odunc DROP COLUMN sube`],
  },
  {
    no: 3,
    ad: 'uye adinin bolunmesi',
    ileri: [
      `ALTER TABLE uye ADD COLUMN soyad TEXT`,
      `UPDATE uye SET soyad = substr(ad, instr(ad, ' ') + 1),
         ad = substr(ad, 1, instr(ad, ' ') - 1) WHERE instr(ad, ' ') > 0`,
    ],
    geri: GERI_SURUMU === 'duzeltilmis'
      ? [`UPDATE uye SET ad = ad || ' ' || soyad WHERE soyad IS NOT NULL AND soyad <> ''`,
         `ALTER TABLE uye DROP COLUMN soyad`]
      : [`ALTER TABLE uye DROP COLUMN soyad`],
  },
  {
    no: 4,
    ad: 'ad ve soyad teklik kisiti',
    ileri: [`CREATE UNIQUE INDEX uye_ad_soyad ON uye(ad, soyad)`],
    geri: [`DROP INDEX uye_ad_soyad`],
  },
];

Koşucu üç iş yapar: numara dizisini denetler, uygulanan adımları bir kayıt tablosunda tutar ve çalıştırdığı sorguları sayar. Sayaç bedelin ölçüsüdür.

// kosucu.mjs — goc kosucusu: numara dizisini denetler, uygulanani kaydeder, sorgu sayar
import { DatabaseSync } from 'node:sqlite';
import { GOCLER } from './gocler.mjs';

export function sirayiDenetle(gocler) {
  const numaralar = gocler.map((g) => g.no);
  const tekil = new Set(numaralar);
  if (tekil.size !== numaralar.length) {
    const yinelenen = numaralar.find((n, i) => numaralar.indexOf(n) !== i);
    throw new Error(`goc numarasi yineleniyor: ${yinelenen}`);
  }
  const sirali = [...tekil].sort((a, b) => a - b);
  sirali.forEach((n, i) => {
    if (n !== i + 1) throw new Error(`goc numarasinda bosluk: ${i + 1} eksik`);
  });
  return sirali.length;
}

export function veritabaniAc(yol = ':memory:') {
  const db = new DatabaseSync(yol);
  db.exec('CREATE TABLE IF NOT EXISTS goc_kaydi (no INTEGER PRIMARY KEY)');
  db.sorgu = 0;
  return db;
}

export const uygulanan = (db) =>
  db.prepare('SELECT no FROM goc_kaydi ORDER BY no').all().map((s) => s.no);

export function ileri(db, hedef) {
  sirayiDenetle(GOCLER);
  for (const goc of GOCLER.filter((g) => g.no <= hedef)) {
    const kayitli = uygulanan(db);
    if (kayitli.includes(goc.no)) continue;
    if (goc.no > 1 && !kayitli.includes(goc.no - 1)) {
      throw new Error(`goc sirasi bozuk: ${goc.no} oncesinde ${goc.no - 1} uygulanmamis`);
    }
    for (const komut of goc.ileri) { db.exec(komut); db.sorgu += 1; }
    db.prepare('INSERT INTO goc_kaydi VALUES (?)').run(goc.no);
    db.sorgu += 1;
  }
  return db;
}

export function geri(db, hedef) {
  for (const goc of [...GOCLER].reverse().filter((g) => g.no > hedef)) {
    if (!uygulanan(db).includes(goc.no)) continue;
    for (const komut of goc.geri) { db.exec(komut); db.sorgu += 1; }
    db.prepare('DELETE FROM goc_kaydi WHERE no = ?').run(goc.no);
    db.sorgu += 1;
  }
  return db;
}

export const sutunlar = (db, tablo) =>
  db.prepare(`PRAGMA table_info(${tablo})`).all().map((s) => `${s.name}:${s.notnull}`);

Dört Test

Birinci test şemanın şeklini doğrular: hangi sütunlar, hangi sırada ve hangisi boş geçilemez. Beklenen liste elle yazılır; göç bir sütun eklediğinde ya da unuttuğunda bu liste ayrışır. İkinci test göç numaralarının tekil ve boşluksuz olduğunu sorar; yakaladığı sınıf, iki ayrı dalda aynı numarayla açılan iki göçtür. Üçüncü test gidiş-dönüş yapar. Dördüncü test göçü var olan veri üzerinde çalıştırır.

// goc.test.mjs — ileri goc, goc numarasi, gidis-donus ve kisit ihlali testleri
import test from 'node:test';
import assert from 'node:assert/strict';
import { GOCLER } from './gocler.mjs';
import { veritabaniAc, ileri, geri, uygulanan, sutunlar, sirayiDenetle } from './kosucu.mjs';

const uyeEkle = (db, no, ad, sube) =>
  db.prepare('INSERT INTO uye (uye_no, ad, sube) VALUES (?, ?, ?)').run(no, ad, sube);

test('ileri goc semayi beklenen bicime getirir', () => {
  const db = ileri(veritabaniAc(), 4);
  assert.deepEqual(uygulanan(db), [1, 2, 3, 4]);
  assert.deepEqual(sutunlar(db, 'uye'), ['uye_no:0', 'ad:1', 'sube:1', 'soyad:0']);
  assert.deepEqual(sutunlar(db, 'odunc'),
    ['kitap_no:0', 'uye_no:1', 'alis_gunu:1', 'son_gun:1', 'sube:0']);
});

test('goc numaralari tekil ve bosluksuz olmali', () => {
  assert.equal(sirayiDenetle(GOCLER), 4);
  const catisan = [...GOCLER, { no: 4, ad: 'baska dalda acilan goc', ileri: [], geri: [] }];
  assert.throws(() => sirayiDenetle(catisan), /goc numarasi yineleniyor: 4/);
});

test('gidis-donus uye adini korur', () => {
  const db = ileri(veritabaniAc(), 2);
  uyeEkle(db, 'U-0001', 'Ayse Demir', 'merkez');
  ileri(db, 3);
  geri(db, 2);
  assert.equal(db.prepare('SELECT ad FROM uye WHERE uye_no = ?').get('U-0001').ad, 'Ayse Demir');
});

test('dorduncu goc yinelenen ad uzerinde kisit ihlaliyle duser', () => {
  const db = ileri(veritabaniAc(), 2);
  uyeEkle(db, 'U-0001', 'Ayse Demir', 'merkez');
  uyeEkle(db, 'U-0002', 'Ayse Demir', 'kampus');
  ileri(db, 3);
  assert.throws(() => ileri(db, 4), /UNIQUE constraint failed/);
});
GERI_GOC=kusurlu node --test --test-reporter=tap goc.test.mjs | grep -E '^ *(ok|not ok|(expected|actual):|# (tests|pass|fail))'
ok 1 - ileri goc semayi beklenen bicime getirir
ok 2 - goc numaralari tekil ve bosluksuz olmali
not ok 3 - gidis-donus uye adini korur
  expected: 'Ayse Demir'
  actual: 'Ayse'
ok 4 - dorduncu goc yinelenen ad uzerinde kisit ihlaliyle duser
# tests 4
# pass 3
# fail 1

Yakalanan kusur sınıfının adı geri göçte veri kaybıdır. Üçüncü göçün ileri yönü adı ikiye bölüyor, geri yönü ise ikinci parçayı taşıyan sütunu doğrudan düşürüyor; soyadı tutan tek yer o sütun olduğu için geri dönüş adı yarım bırakıyor. Birinci testin yeşil kalması bu sınıfın neden kaçtığını da söylüyor: sütun listesi geri göçten sonra doğru görünüyor, taşıdığı değer yanlış.

Düzeltme, sütunu düşürmeden önce içeriğini geri birleştirmektir. Göç kümesinde bu sürüm zaten yazılı.

GERI_GOC=duzeltilmis node --test --test-reporter=tap goc.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - ileri goc semayi beklenen bicime getirir
ok 2 - goc numaralari tekil ve bosluksuz olmali
ok 3 - gidis-donus uye adini korur
ok 4 - dorduncu goc yinelenen ad uzerinde kisit ihlaliyle duser
# tests 4
# pass 4
# fail 0

Dördüncü testin her iki koşumda da geçmesi ayrı bir bilgidir. Teklik kısıtı ekleyen göç, var olan veride yinelenen bir çift bulduğunda düşer; test bunu bir kusur olarak değil, belgelenmiş davranış olarak sabitliyor. Böyle bir göçün üretimde uygulanabilmesi için önünde bir temizleme adımı bulunması gerektiği buradan okunur.

Şema Testinin Görmediği

Birinci test şemanın şeklini doğruluyor: odunc tablosuna sube sütunu eklendi, adı ve sırası doğru. İkinci göçün o sütuna ne yazdığı hiçbir testte sorulmuyor.

// geri-doldurma.mjs — sema testi yesilken ikinci gocun yazdigi degerin ne oldugu
import { veritabaniAc, ileri, sutunlar } from './kosucu.mjs';

const db = ileri(veritabaniAc(), 1);
const uye = db.prepare('INSERT INTO uye (uye_no, ad, sube) VALUES (?, ?, ?)');
const odunc = db.prepare('INSERT INTO odunc (kitap_no, uye_no, alis_gunu, son_gun) VALUES (?, ?, 900, 914)');
for (const [no, ad, sube] of [['U-1', 'Ayse Demir', 'merkez'], ['U-2', 'Emre Koc', 'kampus'], ['U-3', 'Elif Sahin', 'sanat']]) {
  uye.run(no, ad, sube);
  odunc.run(`K-${no}`, no);
}
ileri(db, 2);

const dagilim = (tablo) => db.prepare(`SELECT sube, COUNT(*) AS n FROM ${tablo} GROUP BY sube ORDER BY sube`)
  .all().map((s) => `${s.sube}=${s.n}`).join(' ');

console.log(`sema testinin gordugu : odunc sutunlari ${sutunlar(db, 'odunc').join(' ')}`);
console.log(`uye tablosundaki sube : ${dagilim('uye')}`);
console.log(`goc sonrasi odunc sube: ${dagilim('odunc')}`);
sema testinin gordugu : odunc sutunlari kitap_no:0 uye_no:1 alis_gunu:1 son_gun:1 sube:0
uye tablosundaki sube : kampus=1 merkez=1 sanat=1
goc sonrasi odunc sube: merkez=3

Üyeler üç ayrı şubede, ödünçlerin tamamı merkezde. İkinci göç sütunu sabit bir değerle doldurdu; üyenin şubesinden okumadı. Kaçan kusur sınıfının adı geri doldurma değeri uyuşmazlığıdır ve şema testi onu göremez, çünkü şema testinin kâhini sütun listesidir, satır içerikleri değil. Bu sınıfın yakalanması göç öncesi ve sonrası veri üzerinde değişmez ifadeler kurmayı gerektirir — “her ödüncün şubesi, üyesinin şubesine eşittir” gibi — ve o iddia bir önceki dersin veri kümeleriyle sınanır.

Bedel

// bedel.mjs — goc testinin bedeli: adim, sorgu, dosya ve goreli sure
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { GOCLER } from './gocler.mjs';
import { veritabaniAc, ileri, geri } from './kosucu.mjs';

const kok = mkdtempSync(join(tmpdir(), 'goc-'));
const adim = GOCLER.reduce((t, g) => t + g.ileri.length + g.geri.length, 0);

const sifirdan = performance.now();
const db = ileri(veritabaniAc(join(kok, 'katalog.db')), 4);
const sifirdanSure = performance.now() - sifirdan;
const ileriSorgu = db.sorgu;

const gidisDonus = performance.now();
geri(db, 2);
ileri(db, 4);
const gidisDonusSure = performance.now() - gidisDonus;

const boyut = statSync(join(kok, 'katalog.db')).size;
rmSync(kok, { recursive: true, force: true });

console.log(`goc adimi (ileri + geri komut) : ${adim}`);
console.log(`sifirdan dorde cikan sorgu     : ${ileriSorgu}`);
console.log(`gidis-donus sonrasi toplam     : ${db.sorgu}`);
console.log(`olusan dosya                   : 1, ${boyut} bayt`);
console.log(`gidis-donus sifirdan gocten uzun surmedi : ${gidisDonusSure < sifirdanSure * 3}`);
GERI_GOC=duzeltilmis node bedel.mjs
goc adimi (ileri + geri komut) : 13
sifirdan dorde cikan sorgu     : 11
gidis-donus sonrasi toplam     : 21
olusan dosya                   : 1, 28672 bayt
gidis-donus sifirdan gocten uzun surmedi : true

On üç göç komutu, sıfırdan dörde çıkan on bir sorgu, gidiş-dönüşle birlikte yirmi bir sorgu ve bir dosya. Bu sayılar göç kümesi büyüdükçe doğrusal artar: her yeni adım hem ileri hem geri komutunu, hem de birinci testin beklenen sütun listesinde bir satır değişikliğini getirir. Bakım bedeli buradadır — bir sütun eklemek üç dosyaya dokunmayı gerektirir: göç kümesi, beklenen şema listesi ve o sütunu okuyan sorgular.

Özet

  • Şema testinin kâhini elle yazılan sütun listesidir; göç bir sütunu eklemeyi ya da düşürmeyi unuttuğunda bu liste ayrışır.
  • Göç numaralarının tekilliği ayrı bir testtir ve iki dalda aynı numarayla açılan göçleri yakalar.
  • Gidiş-dönüş testi geri göçte veri kaybını yakaladı: sütunu düşürmeden önce içeriğini birleştirmeyen geri göç, adı yarım bıraktı.
  • Teklik kısıtı ekleyen göç, var olan veride yinelenen bir çift bulduğunda düşer; test bunu belgelenmiş davranış olarak sabitler.
  • Şema testi geri doldurma değerini göremez: sütun eklendi, tüm satırlara sabit bir değer yazıldı ve test yeşil kaldı.

Sonraki Adım

Bu derste gerçek çalıştırılan bağımlılık aynı makinede duruyordu; şeması elde, verisi üretilebilir, göçü tekrarlanabilirdi. Bütün bağımlılıklar böyle değildir. Kütüphanenin ödünç akışı, kimlik doğrulaması yapan ve kitap künyesi döndüren dış bir servise bağlıdır: o servis başka bir ekibindir, testin komutuyla ayağa kalkmaz, her koşumda aynı yanıtı vermeyebilir ve bazı koşumlarda hiç yanıt vermez. Sonraki ders bu bağımlılığı kayıt ve tekrar oynatma ile ele alır: gerçek yanıtlar bir kez kaydedilir, sonraki koşumlar kayıttan beslenir ve bunun hangi kusur sınıfını yakaladığı, hangisini kaçırdığı ölçülü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