İçeriğe geç
academia.sh

Ders 05 / 13

Test Edilebilir Tasarım

Bağımlılığını kendi içinde yaratan sürüm ile bağımlılığı dışarıdan alan sürümün karşılaştırılması, düzenek maliyetinin satırla ölçülmesi ve zaman ile rastgeleliğin dışarıdan verilmesi.

İçindekiler

Önceki derste düzenek testin elindeydi: katalog nesnesi bir fabrika çağrısıyla üretiliyor, her test kendi kopyasını kuruyordu. Bu olanak koddan geldi, testten değil. Sınanan kod kataloğu kendi içinde yaratsaydı, testin elinde onu değiştirecek bir kol kalmazdı.

Test edilebilirlik testin bir niteliği değil, tasarımın bir niteliğidir. Aynı iş kuralı iki farklı biçimde yazıldığında ikisi de doğru çalışır, ama biri sınanabilir bir kol bırakır, diğeri bırakmaz. Bu dersin sorusu şudur: bu fark nerede ortaya çıkar ve ne kadar maliyet üretir?

Aynı Kural, İki Tasarım

Sınanacak iş şudur: ödünç verme işlemi bugünün gün numarasını okur, bir fiş numarası üretir, kitabın halihazırda ödünçte olmadığını denetler ve ödünç kaydını döndürür. İşlemin üç dış kaynağı vardır: saat, fiş numarası üreteci ve katalog.

Birinci sürüm üçünü de kendi içinde yaratır.

// iceriden.mjs — bagimliliklarini kendi iceride yaratan surum
const katalog = new Map();

export function katalogTemizle() {
  katalog.clear();
}

export function oduncVer(uyeNo, kitapNo, uyeTuru) {
  const bugun = Math.floor(Date.now() / 86400000);
  const fisNo = `F-${String(Math.floor(Math.random() * 1000000)).padStart(6, '0')}`;
  if (katalog.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`);
  const sure = uyeTuru === 'ogrenci' ? 28 : 14;
  katalog.set(kitapNo, uyeNo);
  return { fisNo, uyeNo, kitapNo, alisGunu: bugun, sonGun: bugun + sure };
}

İkinci sürüm üçünü de dışarıdan alır. İş kuralı — süre hesabı, çift ödünç denetimi, kayıt biçimi — birebir aynıdır.

// disaridan.mjs — bagimliliklari disaridan verilen surum
export function oduncServisiKur({ saat, fisUretici, katalog }) {
  return {
    oduncVer(uyeNo, kitapNo, uyeTuru) {
      const bugun = saat();
      const fisNo = fisUretici();
      if (katalog.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`);
      const sure = uyeTuru === 'ogrenci' ? 28 : 14;
      katalog.set(kitapNo, uyeNo);
      return { fisNo, uyeNo, kitapNo, alisGunu: bugun, sonGun: bugun + sure };
    },
  };
}

Bu ikinci biçime bağımlılık enjeksiyonu (dependency injection) denir; bağımlılık içeride yaratılmak yerine dışarıdan verilir. Adı kulağa bir çatı özelliği gibi gelir, ama burada görüldüğü gibi yalnız bir imza kararıdır: kod bağımlılığını aramak yerine kendisine verilmesini bekler.

Düzeneğin Maliyeti

İki sürümün testleri aynı kuralı sınar; farkı, o kurala ulaşmak için kurulması gereken düzenek yaratır.

// duzenek.mjs — iki surumun testinin kurdugu duzenek
import { katalogTemizle, oduncVer } from './iceriden.mjs';
import { oduncServisiKur } from './disaridan.mjs';

export function iceridenDuzenek() {
  const gercekNow = Date.now;
  const gercekRandom = Math.random;
  Date.now = () => 1000 * 86400000;
  Math.random = () => 0.123456;
  katalogTemizle();
  const geriAl = () => {
    Date.now = gercekNow;
    Math.random = gercekRandom;
  };
  return { servis: { oduncVer }, geriAl };
}

export function disaridanDuzenek() {
  const servis = oduncServisiKur({
    saat: () => 1000,
    fisUretici: () => 'F-123456',
    katalog: new Map(),
  });
  return { servis, geriAl: () => {} };
}

Düzeneklerin boyutu ölçülebilir. İki düzenek de çalıştırılıp ürettikleri kayıt karşılaştırıldığında, farkın davranışta değil yalnız hazırlıkta olduğu görülür.

// olcum.mjs — iki duzenegin satir sayisi ve urettikleri kayit
import { iceridenDuzenek, disaridanDuzenek } from './duzenek.mjs';

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

for (const [ad, duzenek] of [['iceriden', iceridenDuzenek], ['disaridan', disaridanDuzenek]]) {
  const { servis, geriAl } = duzenek();
  const kayit = servis.oduncVer('U-17', 'K-903', 'ogrenci');
  geriAl();
  console.log(`${ad.padEnd(10)} duzenek ${String(satir(duzenek)).padStart(2)} satir  ${JSON.stringify(kayit)}`);
}
iceriden   duzenek 10 satir  {"fisNo":"F-123456","uyeNo":"U-17","kitapNo":"K-903","alisGunu":1000,"sonGun":1028}
disaridan  duzenek  6 satir  {"fisNo":"F-123456","uyeNo":"U-17","kitapNo":"K-903","alisGunu":1000,"sonGun":1028}

İki kayıt birebir aynı. Düzenek maliyeti on satıra karşı altı satır. Ama asıl fark satır sayısı değil, o satırların ne yaptığıdır. Dışarıdan verilen sürümün düzeneği yalnız nesne kurar. İçeriden yaratan sürümün düzeneği ise iki küresel işlevi değiştirir, sınanan modülün iç durumunu temizler ve değiştirdiklerini geri almayı üstlenir.

Birinci derste “hazırla bölümünün uzaması ayrı bir sinyaldir” denmişti. Ölçülen sinyal budur: düzenek büyüdükçe testin sınadığı kural azalmaz, ama teste giren risk artar.

Geri Alınmayan Değişikliğin Bedeli

Küresel bir işlevi değiştiren düzeneğin taşıdığı risk somuttur. Aşağıdaki test dosyasında birinci test saati dondurur ve geri almayı unutur.

// sizinti.test.mjs — geri alinmayan kuresel degisiklik sonraki testi etkiler
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { katalogTemizle, oduncVer } from './iceriden.mjs';

test('ogrenci oduncu yirmi sekiz gun surer', () => {
  Date.now = () => 1000 * 86400000;
  katalogTemizle();

  const kayit = oduncVer('U-17', 'K-903', 'ogrenci');

  assert.equal(kayit.sonGun, 1028);
});

test('gun numarasi gercek saatten gelir', () => {
  const bugun = Math.floor(Date.now() / 86400000);

  assert.ok(bugun > 20000, `beklenmedik gun numarasi: ${bugun}`);
});
node --test --test-reporter=tap sizinti.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - ogrenci oduncu yirmi sekiz gun surer
not ok 2 - gun numarasi gercek saatten gelir
# tests 2
# pass 1
# fail 1

İkinci test kendi başına doğrudur ve tek başına koşturulduğunda geçer. Düştüğü koşumda gördüğü gün numarası bindir; çünkü saat hâlâ birinci testin bıraktığı gibi donmuştur. Bu, önceki dersteki sıra bağımlılığının aynısıdır, ama kaynağı farklıdır: paylaşılan durum kitaplığın kendisinde değil, çalışma zamanının küresel nesnelerinde birikiyor.

Küresel değişikliği geri almak bir çözümdür, ama unutulabilir bir çözümdür ve unutulduğunda bozulan test, hatayı yapan test değildir. Dışarıdan verilen sürümde bu risk sınıfı hiç oluşmaz.

// temiz.test.mjs — disaridan verilen bagimlilik kuresel durumu bozmaz
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { oduncServisiKur } from './disaridan.mjs';

const kur = () => oduncServisiKur({
  saat: () => 1000,
  fisUretici: () => 'F-123456',
  katalog: new Map(),
});

test('ogrenci oduncu yirmi sekiz gun surer', () => {
  const servis = kur();

  const kayit = servis.oduncVer('U-17', 'K-903', 'ogrenci');

  assert.equal(kayit.sonGun, 1028);
});

test('gun numarasi gercek saatten gelir', () => {
  const bugun = Math.floor(Date.now() / 86400000);

  assert.ok(bugun > 20000, `beklenmedik gun numarasi: ${bugun}`);
});

test('fis numarasi dis kaynaktan gelir', () => {
  const servis = kur();

  const kayit = servis.oduncVer('U-17', 'K-903', 'uye');

  assert.equal(kayit.fisNo, 'F-123456');
});
node --test --test-reporter=tap temiz.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - ogrenci oduncu yirmi sekiz gun surer
ok 2 - gun numarasi gercek saatten gelir
ok 3 - fis numarasi dis kaynaktan gelir
# tests 3
# pass 3
# fail 0

Üçüncü test, ilk iki sürümde hiç yazılamayan bir şeyi sınıyor: fiş numarasının nereden geldiği. İçeriden yaratan sürümde fiş numarası rastgelelik kaynağından gelir ve testin onu doğrulaması için yine küresel bir değişiklik gerekir. Dışarıdan verilen sürümde üreteç bir parametredir ve testin verdiği değer aynen kayda geçer.

Belirlenimci Olmayan Kaynakların Yeri

Zaman ve rastgelelik, testin denetleyemediği kaynakların iki tipik örneğidir; aynı sınıfa süreç kimliği, ortam değişkenleri, ağ adresleri ve dosya sistemi de girer. Ortak yanları, aynı girdiye aynı çıktıyı vermemeleridir.

Bunları sınanabilir kılmanın yolu, kodun bu kaynaklara doğrudan erişmesini engellemek değil, erişimi tek bir noktada toplayıp o noktayı dışarıdan verilebilir hâle getirmektir. Yukarıdaki sürümde saat ve fisUretici bu noktalardır. Üretimde çalışan yapılandırma gerçek kaynakları verir; testte çalışan yapılandırma sabit değerler verir.

Bu ayrımın bir sınırı vardır ve baştan söylenmelidir: bir kod parçası zamanı gerçekten okumak zorundaysa, o okuma da bir yerde sınanmalıdır. Dışarıdan verme, o sınamayı ortadan kaldırmaz — birim testinin dışına, tümleşim testine taşır. Kazanç, iş kuralının belirlenimci bir bağlamda sınanabilmesidir.

Özet

  • Test edilebilirlik testin değil tasarımın niteliğidir; aynı iş kuralı iki biçimde de doğru çalışır, ama yalnız biri sınanabilir bir kol bırakır.
  • Bağımlılığın dışarıdan verilmesi bir çatı özelliği değil bir imza kararıdır: kod bağımlılığını aramak yerine kendisine verilmesini bekler.
  • Ölçümde iki düzenek aynı kaydı üretti; içeriden yaratan sürümün düzeneği on, dışarıdan alan sürümünki altı satırdı ve fark yalnız uzunlukta değil, küresel değişiklik içermesindeydi.
  • Geri alınmayan küresel değişiklik sonraki testi düşürür; bozulan test, hatayı yapan test değildir.
  • Zaman, rastgelelik ve benzeri belirlenimci olmayan kaynaklar tek bir noktada toplanıp dışarıdan verilebilir kılınır; bu, o kaynakların sınanmasını kaldırmaz, tümleşim testine taşır.

Sonraki Adım

Bu derste dışarıdan verilen bağımlılıklar sabit değer döndüren küçük işlevlerdi: saat her zaman bin, fisUretici her zaman aynı numarayı verdi. Bu, dört türü olan bir ailenin yalnız bir üyesidir. Bir bağımlılık yalnız yer doldurmak için de verilebilir, çağrıldığında kayıt tutmak için de, gerçek davranışın çalışan ama basitleştirilmiş bir kopyası olmak için de. Sonraki ders bu türleri tek tek elle yazar ve her birinin hangi soruyu yanıtladığını ayırı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