İçeriğe geç
academia.sh

Ders 01 / 15

Tümleşim Testinin Kapsamı

Hangi bağımlılığın gerçeğiyle koşturulacağının bir karar olarak ele alınması: aynı testin sahte ve gerçek katalogla koşturulması, şema uyuşmazlığının yalnız gerçek sınırda yakalanması ve sınırın bedelinin sorgu sayısıyla ölçülmesi.

İçindekiler

Birim Testi ve Test Odaklı Geliştirme kursunun kapanışında bir varsayımın adı konmuştu: oradaki her test süreç içinde ve gerçek bağımlılık olmadan koştu, sahte bağımlılıkların gerçeğine uyduğu varsayıldı ama hiçbir yerde sınanmadı. Bu kurs o varsayımı sınar. İlk soru, sınamanın nereden başlayacağıdır.

Yanıt bir erdem değil, bir karardır. “Gerçeğiyle test etmek daha güvenli” cümlesi hiçbir şey seçmez; her bağımlılık için gerçeğin çalıştırılması ayrı bir bedel getirir ve ayrı bir kusur sınıfını görünür kılar. Bu ders kararı bir örnek üzerinde verir ve üç şeyi birlikte ölçer: seçilen sınırın yakaladığı kusur sınıfı, yakalamadığı kusur sınıfı ve bedeli. Kurs boyunca ölçü ekseni budur.

Sınır Kararı

Ödünç servisinin dört bağımlılığı vardı: kayıtları tutan katalog, üyeye haber veren bildirim kanalı, işlem günlüğü ve gün numarasını veren saat. Her biri için iki seçenek bulunur — sahte bağımlılık ya da gerçeği — ve dördü bağımsız seçildiğinde on altı yapılandırma çıkar. Bunların hepsi anlamlı değildir. Saatin gerçeği testte hiçbir kusur sınıfı açığa çıkarmaz, yalnız belirlenimciliği yok eder. Bildirim kanalının gerçeği bir süreç ayağa kaldırmayı gerektirir. Katalog ise ikisinin arasındadır: gerçeği süreç içinde çalışabilen bir veri tabanıdır ve kendi kısıtlarını taşır.

Bu ders katalog sınırını gerçek çalıştırır, kalanını sahte bırakır. Bu yapılandırmanın adı tümleşim testidir (integration test): sınanan kod ile en az bir bağımlılığın gerçek gerçekleştirimi birlikte koşar.

Aynı Testin İki Yapılandırması

Ödünç servisi, önceki kurstan geldiği biçimiyle duruyor.

// servis.mjs — M21/K02'den gelen odunc servisi: katalog, bildirim ve saat bagimliliklariyla
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export function oduncServisiKur({ katalog, bildirim, saat }) {
  return {
    oduncVer(uye, kitapNo) {
      const kural = KURALLAR[uye.tur];
      if (katalog.uyeOduncSayisi(uye.no) >= kural.enCokOdunc) {
        throw new Error(`odunc siniri asildi: ${kural.enCokOdunc}`);
      }
      const bugun = saat();
      const kayit = { uyeNo: uye.no, kitapNo, alisGunu: bugun, sonGun: bugun + kural.oduncGun };
      katalog.ekle(kayit);
      bildirim.gonder(uye.no, `son tarih: ${kayit.sonGun}`);
      return kayit;
    },
    iadeAl(kitapNo) {
      const kayit = katalog.bul(kitapNo);
      if (kayit === undefined) throw new Error(`odunc kaydi yok: ${kitapNo}`);
      katalog.sil(kitapNo);
      return kayit;
    },
  };
}

Katalog arayüzünün iki gerçekleştirimi yazılır. Birincisi önceki kurstaki taklit nesnedir. İkincisi aynı arayüzü gerçek bir şema üzerinde karşılar; şema, üretimde kullanılan sürümdür ve kataloğu yazan ekipten gelir.

// kataloglar.mjs — ayni katalog arayuzunun iki gerceklestirimi: taklit nesne ve node:sqlite
import { DatabaseSync } from 'node:sqlite';

export const SEMA = `
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,
  CHECK (son_gun > alis_gunu)
);`;

export function taklitKatalog() {
  const kayitlar = new Map();
  return {
    uyeOduncSayisi: (uyeNo) => [...kayitlar.values()].filter((k) => k.uyeNo === uyeNo).length,
    ekle: (kayit) => kayitlar.set(kayit.kitapNo, kayit),
    bul: (kitapNo) => kayitlar.get(kitapNo),
    sil: (kitapNo) => kayitlar.delete(kitapNo),
    sorguSayisi: () => 0,
  };
}

export function sqliteKatalog(yol = ':memory:') {
  const db = new DatabaseSync(yol);
  db.exec(SEMA);
  let sorgu = 0;
  const calistir = (metin, ...bagli) => { sorgu += 1; return db.prepare(metin).run(...bagli); };
  const oku = (metin, ...bagli) => { sorgu += 1; return db.prepare(metin).get(...bagli); };
  return {
    uyeOduncSayisi: (uyeNo) => oku('SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?', uyeNo).n,
    ekle: (kayit) => calistir(
      'INSERT INTO odunc (kitap_no, uye_no, sube, alis_gunu, son_gun) VALUES (?, ?, ?, ?, ?)',
      kayit.kitapNo, kayit.uyeNo, kayit.sube ?? null, kayit.alisGunu, kayit.sonGun),
    bul: (kitapNo) => {
      const s = oku('SELECT kitap_no, uye_no, alis_gunu, son_gun FROM odunc WHERE kitap_no = ?', kitapNo);
      return s && { kitapNo: s.kitap_no, uyeNo: s.uye_no, alisGunu: s.alis_gunu, sonGun: s.son_gun };
    },
    sil: (kitapNo) => calistir('DELETE FROM odunc WHERE kitap_no = ?', kitapNo),
    sorguSayisi: () => sorgu,
  };
}

Düzenek hangi gerçekleştirimin kurulacağını tek bir yerden seçer. Bildirim bir casus nesnedir, saat bir işlevdir; ikisi de önceki kurstaki biçimini korur.

// duzenek.mjs — ayni testin iki yapilandirmasi: katalog sahte ya da gercek
import { oduncServisiKur } from './servis.mjs';
import { taklitKatalog, sqliteKatalog } from './kataloglar.mjs';

export function duzenekKur(yapilandirma) {
  const katalog = yapilandirma === 'gercek' ? sqliteKatalog() : taklitKatalog();
  const gonderilen = [];
  const bildirim = { gonder: (uyeNo, metin) => gonderilen.push({ uyeNo, metin }) };
  const servis = oduncServisiKur({ katalog, bildirim, saat: () => 1000 });
  return { servis, katalog, gonderilen };
}

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

Testler tek dosyadır ve yapılandırmayı bir ortam değişkeninden alır. Üçüncü test, sınır denetimini sınadığı için kataloğa doğrudan beş kayıt yazar; o kayıtları yazan taraf şemayı bildiğinden sube alanını verir.

// odunc.test.mjs — ayni uc test; KATALOG degiskeni sinirin gercek calisip calismadigini secer
import test from 'node:test';
import assert from 'node:assert/strict';
import { duzenekKur, UYE } from './duzenek.mjs';

const yapilandirma = process.env.KATALOG ?? 'sahte';

test('yeni odunc katalogda gorunur', () => {
  const { servis, katalog } = duzenekKur(yapilandirma);
  servis.oduncVer(UYE, 'K-903');
  assert.equal(katalog.bul('K-903').sonGun, 1014);
});

test('iade edilen kitap katalogdan duser', () => {
  const { servis, katalog } = duzenekKur(yapilandirma);
  servis.oduncVer(UYE, 'K-903');
  servis.iadeAl('K-903');
  assert.equal(katalog.bul('K-903'), undefined);
});

test('odunc siniri asilinca hata verilir', () => {
  const { servis, katalog } = duzenekKur(yapilandirma);
  for (let i = 0; i < 5; i += 1) {
    katalog.ekle({ kitapNo: `K-90${i}`, uyeNo: UYE.no, sube: 'merkez', alisGunu: 1000, sonGun: 1014 });
  }
  assert.throws(() => servis.oduncVer(UYE, 'K-999'), /odunc siniri asildi: 5/);
});

Sahte Sınırda Yeşil

Önceki kursun bıraktığı yapılandırma budur: katalog bir taklit nesnedir.

KATALOG=sahte node --test --test-reporter=tap odunc.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - yeni odunc katalogda gorunur
ok 2 - iade edilen kitap katalogdan duser
ok 3 - odunc siniri asilinca hata verilir
# tests 3
# pass 3
# fail 0

Üç test de geçiyor. Bu koşum, servisin doğru olduğunu değil, servisin taklit kataloğun kabul ettiği her şeyi yaptığını gösterir. Taklit katalog bir eşlemedir ve bir eşleme her biçimdeki kaydı kabul eder.

Gerçek Sınırda Kırmızı

Tek değişiklik, katalog sınırının gerçeğiyle koşturulmasıdır. Test dosyası, iddialar ve servis aynen duruyor.

KATALOG=gercek node --test --test-reporter=tap odunc.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
not ok 1 - yeni odunc katalogda gorunur
not ok 2 - iade edilen kitap katalogdan duser
ok 3 - odunc siniri asilinca hata verilir
# tests 3
# pass 1
# fail 2

İlk iki test düştü. Düşme nedeni koşumun ayrıntısında yazılıdır.

KATALOG=gercek node --test --test-reporter=tap odunc.test.mjs | grep "error:" | sort -u
  error: 'NOT NULL constraint failed: odunc.sube'

Yakalanan kusur sınıfının adı şema uyuşmazlığıdır: servisin ürettiği kayıt, gerçek şemanın zorunlu tuttuğu bir alanı taşımıyor. Bu kusur koddaydı ve önceki kursun tamamı boyunca oradaydı; taklit katalog onu göremezdi, çünkü taklit nesnenin kabul kuralı yoktur. Üçüncü testin yeşil kalması da bilgi taşır: kayıtları şemayı bilerek yazan taraf hatasızdır, kusur yalnız servisin yazdığı yoldadır.

Düzeltme servisin kayıt biçimindedir.

// servis.mjs — kayit artik sube alanini da tasiyor
export const KURALLAR = {
  ogrenci: { oduncGun: 28, enCokOdunc: 10 },
  uye: { oduncGun: 14, enCokOdunc: 5 },
};

export function oduncServisiKur({ katalog, bildirim, saat }) {
  return {
    oduncVer(uye, kitapNo) {
      const kural = KURALLAR[uye.tur];
      if (katalog.uyeOduncSayisi(uye.no) >= kural.enCokOdunc) {
        throw new Error(`odunc siniri asildi: ${kural.enCokOdunc}`);
      }
      const bugun = saat();
      const kayit = {
        uyeNo: uye.no, kitapNo, sube: uye.sube,
        alisGunu: bugun, sonGun: bugun + kural.oduncGun,
      };
      katalog.ekle(kayit);
      bildirim.gonder(uye.no, `son tarih: ${kayit.sonGun}`);
      return kayit;
    },
    iadeAl(kitapNo) {
      const kayit = katalog.bul(kitapNo);
      if (kayit === undefined) throw new Error(`odunc kaydi yok: ${kitapNo}`);
      katalog.sil(kitapNo);
      return kayit;
    },
  };
}
KATALOG=gercek node --test --test-reporter=tap odunc.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - yeni odunc katalogda gorunur
ok 2 - iade edilen kitap katalogdan duser
ok 3 - odunc siniri asilinca hata verilir
# tests 3
# pass 3
# fail 0

Bu Testin Görmediği

Katalog sınırı gerçek çalışıyor, bildirim sınırı hâlâ bir casus nesne. Bu, bir kusur sınıfını dokunulmadan bırakır. Bildirimin muhatabına üye numarası yerine üye türünü yazan bir sürüm ele alınsın.

// kusurlu.mjs — bildirimin muhatabina uye numarasi yerine uye turu yazan surum
import { KURALLAR } from './servis.mjs';

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

Test, bildirimi de doğruluyor: bir bildirim gitmiş olmalı ve metni doğru son günü taşımalı.

// kacan.test.mjs — katalog gercek, bildirim casus nesne; muhatap kusuru sinanmis olmuyor
import test from 'node:test';
import assert from 'node:assert/strict';
import { sqliteKatalog } from './kataloglar.mjs';
import { kusurluServisKur } from './kusurlu.mjs';

test('odunc kaydi yazilir ve bir bildirim gonderilir', () => {
  const katalog = sqliteKatalog();
  const gonderilen = [];
  const bildirim = { gonder: (muhatap, metin) => gonderilen.push({ muhatap, metin }) };
  const servis = kusurluServisKur({ katalog, bildirim, saat: () => 1000 });

  servis.oduncVer({ no: 'U-17', tur: 'uye', sube: 'merkez' }, 'K-903');

  assert.equal(katalog.bul('K-903').uyeNo, 'U-17');
  assert.equal(gonderilen.length, 1);
  assert.match(gonderilen[0].metin, /son tarih: 1014/);
  console.log(`gonderilen muhatap: ${gonderilen[0].muhatap}`);
});
node --test --test-reporter=tap kacan.test.mjs | grep -E '^ *(ok|not ok|# (gonderilen|tests|pass|fail))'
# gonderilen muhatap: uye
ok 1 - odunc kaydi yazilir ve bir bildirim gonderilir
# tests 1
# pass 1
# fail 0

Test yeşil, muhatap yanlış. Kaçan kusur sınıfının adı muhatap kimliği uyuşmazlığıdır: gerçek bildirim kanalı tanımadığı bir muhatap için hata döndürürdü, casus nesne ise kendisine verilen her değeri kaydeder. Bu sınıfın yakalanması bildirim sınırının da gerçek çalıştırılmasını gerektirir; katalog sınırını gerçek yapmak buna hiçbir katkı sağlamaz. Buradan çıkan kural, kursun tamamına yayılır: bir testin gördüğü kusur sınıfı, gerçek çalıştırdığı sınırla belirlenir.

Bedel

Gerçek sınırın karşılığı ölçülebilir. İki nicelik koşumdan bağımsızdır — koşum başına çalışan sorgu sayısı ve düzeneğin satır sayısı — üçüncüsü, süre, makineye bağlıdır ve ham milisaniye yerine karşılaştırma olarak bildirilir.

// bedel.mjs — iki yapilandirmanin sorgu sayisi, duzenek satiri ve goreli suresi
import { duzenekKur, UYE } from './duzenek.mjs';
import { taklitKatalog, sqliteKatalog } from './kataloglar.mjs';

const TUR = 500;
const satir = (islev) => islev.toString().split('\n').length - 2;

function olc(yapilandirma) {
  const ilk = duzenekKur(yapilandirma);
  ilk.servis.oduncVer(UYE, 'K-903');
  ilk.servis.iadeAl('K-903');
  const baslangic = performance.now();
  for (let i = 0; i < TUR; i += 1) {
    const { servis } = duzenekKur(yapilandirma);
    servis.oduncVer(UYE, 'K-903');
    servis.iadeAl('K-903');
  }
  return { sure: performance.now() - baslangic, sorgu: ilk.katalog.sorguSayisi() };
}

const sahte = olc('sahte');
const gercek = olc('gercek');

console.log(`sahte katalog : ${TUR} tur, tur basina ${sahte.sorgu} sorgu, katalog ${satir(taklitKatalog)} satir`);
console.log(`gercek katalog: ${TUR} tur, tur basina ${gercek.sorgu} sorgu, katalog ${satir(sqliteKatalog)} satir`);
console.log(`gercek sinir daha uzun surdu      : ${gercek.sure > sahte.sure}`);
console.log(`sure farki en az on kat            : ${gercek.sure / sahte.sure >= 10}`);
sahte katalog : 500 tur, tur basina 0 sorgu, katalog 8 satir
gercek katalog: 500 tur, tur basina 4 sorgu, katalog 17 satir
gercek sinir daha uzun surdu      : true
sure farki en az on kat            : true

Bir ödünç ile bir iade turu, gerçek sınırda dört sorgu çalıştırıyor; sahte sınırda hiç sorgu yok. Katalog gerçekleştirimi sekiz satırdan on yedi satıra çıkıyor ve bu on yedi satır artık bakım gerektiren bir düzenek kodudur: şema değişirse burası da değişir. Süre farkının mutlak değeri makineye ve dosya sistemine bağlıdır, yönü değildir.

Bu üç sayı kararı taşır. Dört sorgu ve on kat süre, bir kusur sınıfının — şema uyuşmazlığının — karşılığında ödenmiştir. Aynı bedeli saat sınırı için ödemek hiçbir kusur sınıfı kazandırmaz; bu yüzden saat sahte kalır.

Özet

  • Hangi sınırın gerçek çalıştırılacağı bağımlılık başına verilen bir karardır; dört bağımlılık on altı yapılandırma üretir ve hepsi anlamlı değildir.
  • Taklit kataloğun kabul kuralı yoktur; gerçek şemanın vardır. Önceki kurs boyunca yeşil kalan servis, gerçek şemayla iki testte düştü ve kusurun adı şema uyuşmazlığıdır.
  • Bir testin göremediği kusur sınıfı, gerçek çalıştırmadığı sınırla belirlenir: bildirim casus nesne kaldığı için muhatap kimliği uyuşmazlığı yeşil kaldı.
  • Sınırın bedeli koşumdan bağımsız niceliklerle ölçülür: tur başına dört sorgu ve dokuz satırlık ek düzenek kodu.
  • Bu kursun her dersi aynı üçlüyü yazar: yakalanan kusur sınıfı, kaçan kusur sınıfı ve bedel.

Sonraki Adım

Katalog sınırı süreç içinde 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 testten önce ayağa kalkmış, testten sonra indirilmiş olması gerekir. Ayrıca kataloğun kendisi de bellekte değil bir dosyada durursa, şemanın o dosyada kurulmuş olması bir kurulum adımıdır. Sonraki ders bu adımları test ortamının yönetimi olarak ele alır: bağımlı servis nasıl ayağa kaldırılır, hazır olduğu nasıl anlaşılır ve bu düzeneğin her koşumda ödenen bedeli kaç süreç, kaç adım eder.

İ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