Ders 06 / 13
Sahte Bağımlılıklar
Boş nesne, güdük, taklit nesne, casus nesne ve sahte nesne türlerinin elle yazılması, her türün yanıtladığı sorunun ayrılması ve aynı değişiklikte hangi türün konuştuğunun ölçülmesi.
İçindekiler
Önceki derste dışarıdan verilen bağımlılıklar sabit bir değer döndüren küçük işlevlerdi. Bu, bir ailenin tek üyesidir. Sınanan koda gerçek bağımlılık yerine konan nesnelerin ortak adı sahte bağımlılıktır (test double) ve aralarında yapı değil amaç farkı vardır: bir tanesi yalnız parametre yerini doldurur, bir tanesi belirli bir yanıt verir, bir tanesi gerçek davranışın çalışan bir kopyasıdır, bir tanesi çağrıları kaydeder, bir tanesi de beklentiyi kendi içinde taşır.
Bu ayrımlar akademik değildir. Her tür farklı bir doğrulama sorusunu yanıtlar ve yanlış tür seçildiğinde test ya hiçbir şey sınamaz ya da sınamak istemediği bir şeye bağlanır. Bu ders beş türü de elle yazar ve sonunda hepsini aynı değişiklik karşısında sınar.
Sınanan Servis
Ödünç servisi dört bağımlılık alır: kayıtları tutan katalog, üyeye haber veren bildirim kanalı, işlem günlüğü ve gün numarasını veren saat.
// servis.mjs — odunc servisi: katalog, bildirim, gunluk ve saat bagimliliklariyla export const KURALLAR = { ogrenci: { oduncGun: 28, enCokOdunc: 10 }, uye: { oduncGun: 14, enCokOdunc: 5 }, }; export function oduncServisiKur({ katalog, bildirim, gunluk, 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); gunluk.yaz(`iade ${kitapNo}`); return kayit; }, }; }
Beş Türün Yazımı
Beşi de birkaç satırdır; hiçbiri bir kütüphane gerektirmez.
// sahteler.mjs — dort sahte bagimlilik turu ve beklentili bir sahte nesne export function bosNesne(ad) { return new Proxy({}, { get: (_hedef, alan) => () => { throw new Error(`${ad}.${String(alan)} cagrilmamaliydi`); }, }); } export function gudukKatalog(oduncSayisi) { return { uyeOduncSayisi: () => oduncSayisi, ekle() {}, bul: () => undefined, sil() {}, }; } 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), }; } export function casusNesne(...yontemler) { const cagrilar = []; const nesne = { cagrilar }; for (const ad of yontemler) nesne[ad] = (...girdi) => cagrilar.push([ad, ...girdi]); return nesne; } export function sahteNesne(beklenen, ...yontemler) { const casus = casusNesne(...yontemler); return { ...casus, dogrula() { const gercek = JSON.stringify(casus.cagrilar); const bek = JSON.stringify(beklenen); if (gercek === bek) return; throw new Error(`beklenti karsilanmadi: beklenen ${bek}, gercek ${gercek}`); }, }; }
Kukla nesne (dummy) yalnız parametre yerini doldurur; çağrılmaması beklenir. Buradaki sürüm bu beklentiyi zorunlu kılar: herhangi bir yöntemi çağrıldığında hata fırlatır.
Güdük (stub) sınanan kodun okuduğu bir değeri sabitler. Sorusu şudur: kod bu girdiyle hangi yolu izler? Güdüğün kendisi hiçbir şey doğrulamaz.
Taklit nesne (fake) gerçek bağımlılığın çalışan ama basitleştirilmiş bir kopyasıdır. Buradaki katalog gerçek bir eşleme kullanır; ekleme, arama ve silme anlamlıdır. Çok adımlı senaryolar ancak bu türle kurulabilir.
Casus nesne (spy) çağrıları kaydeder ve testin sonradan incelemesine bırakır. Sorusu şudur: bu çağrı gerçekten yapıldı mı, hangi bağımsız değişkenlerle?
Sahte nesne (mock) beklentiyi kendi içinde taşır. Casus nesneden farkı, doğrulamanın testin değil nesnenin sorumluluğunda olmasıdır.
Her Türün Yanıtladığı Soru
Beş tür, beş ayrı testte kullanıldığında farkları görünür olur.
// sahteler.test.mjs — her sahte bagimlilik turu ayri bir soruyu yanitlar import { test } from 'node:test'; import assert from 'node:assert/strict'; import { oduncServisiKur } from './servis.mjs'; import { bosNesne, gudukKatalog, taklitKatalog, casusNesne, sahteNesne } from './sahteler.mjs'; const OGRENCI = { no: 'U-17', tur: 'ogrenci' }; const UYE = { no: 'U-42', tur: 'uye' }; test('bos nesne: odunc verme gunluge hic dokunmaz', () => { const servis = oduncServisiKur({ katalog: taklitKatalog(), bildirim: casusNesne('gonder'), gunluk: bosNesne('gunluk'), saat: () => 1000, }); const kayit = servis.oduncVer(OGRENCI, 'K-903'); assert.equal(kayit.sonGun, 1028); }); test('guduk: siniri dolu uyede odunc reddedilir', () => { const servis = oduncServisiKur({ katalog: gudukKatalog(5), bildirim: bosNesne('bildirim'), gunluk: bosNesne('gunluk'), saat: () => 1000, }); assert.throws(() => servis.oduncVer(UYE, 'K-903'), /odunc siniri asildi: 5/); }); test('taklit nesne: iade edilen kitap yeniden odunc verilebilir', () => { const servis = oduncServisiKur({ katalog: taklitKatalog(), bildirim: casusNesne('gonder'), gunluk: casusNesne('yaz'), saat: () => 1000, }); servis.oduncVer(OGRENCI, 'K-903'); servis.iadeAl('K-903'); const ikinci = servis.oduncVer(UYE, 'K-903'); assert.equal(ikinci.sonGun, 1014); }); test('casus nesne: uyeye tek bildirim gider', () => { const casus = casusNesne('gonder'); const servis = oduncServisiKur({ katalog: taklitKatalog(), bildirim: casus, gunluk: bosNesne('gunluk'), saat: () => 1000, }); servis.oduncVer(OGRENCI, 'K-903'); assert.deepEqual(casus.cagrilar, [['gonder', 'U-17', 'son tarih: 1028']]); }); test('sahte nesne: reddedilen oduncte bildirim gitmez', () => { const sahte = sahteNesne([], 'gonder'); const servis = oduncServisiKur({ katalog: gudukKatalog(5), bildirim: sahte, gunluk: bosNesne('gunluk'), saat: () => 1000, }); assert.throws(() => servis.oduncVer(UYE, 'K-903')); sahte.dogrula(); });
node --test --test-reporter=tap sahteler.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - bos nesne: odunc verme gunluge hic dokunmaz ok 2 - guduk: siniri dolu uyede odunc reddedilir ok 3 - taklit nesne: iade edilen kitap yeniden odunc verilebilir ok 4 - casus nesne: uyeye tek bildirim gider ok 5 - sahte nesne: reddedilen oduncte bildirim gitmez # tests 5 # pass 5 # fail 0
Beş testin doğrula bölümleri birbirinden farklıdır ve fark tür seçiminden gelir. Birinci testin iddiası dönen kayıtla ilgilidir; kukla nesne yalnız bir varsayımı korur. İkinci test yine dönüş değerine bakar; güdük ise o dönüşü mümkün kılan girdiyi verir. Üçüncü test üç adımlık bir senaryo kurar ve bu ancak durum tutan bir taklit nesneyle olur. Dördüncü ve beşinci testler dönüş değerine değil, yapılan çağrıya bakar.
Son iki testin ayrımı incedir. Casus nesne kullanan test doğrulamayı kendi gövdesinde
yapar ve iddia açıkça okunur. Sahte nesne kullanan test beklentiyi düzenekte kurar ve
gövdesinde yalnız dogrula() çağırır. İkincisi, birden çok çağrı beklentisi olan
durumlarda gövdeyi kısaltır; karşılığında beklentinin nerede kurulduğu testin başına taşınır.
Aynı Değişiklikte Kim Konuşur
Türlerin gerçek farkı, kod değiştiğinde hangisinin ses çıkardığıdır. Aşağıdaki betik servise iki yeni yan etki ekler — her ödünç denemesi günlüğe yazılır ve reddedilen istekte de üyeye bildirim gider — ve aynı üç türü bu yeni sürüme uygular.
// sinama.mjs — servis yan etki kazandiginda hangi sahte bagimlilik konusur import assert from 'node:assert/strict'; import { KURALLAR } from './servis.mjs'; import { bosNesne, gudukKatalog, sahteNesne } from './sahteler.mjs'; function degismisServisKur({ katalog, bildirim, gunluk, saat }) { return { oduncVer(uye, kitapNo) { const kural = KURALLAR[uye.tur]; gunluk.yaz(`odunc denemesi ${kitapNo}`); if (katalog.uyeOduncSayisi(uye.no) >= kural.enCokOdunc) { bildirim.gonder(uye.no, 'odunc reddedildi'); 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; }, }; } const UYE = { no: 'U-42', tur: 'uye' }; const sessizGunluk = { yaz() {} }; const sessizBildirim = { gonder() {} }; function dene(ad, govde) { try { govde(); console.log(`${ad.padEnd(12)} sessiz`); } catch (hata) { console.log(`${ad.padEnd(12)} ${hata.message.split('\n')[0]}`); } } dene('guduk', () => { const servis = degismisServisKur({ katalog: gudukKatalog(5), bildirim: sessizBildirim, gunluk: sessizGunluk, saat: () => 1000, }); assert.throws(() => servis.oduncVer(UYE, 'K-903'), /odunc siniri asildi: 5/); }); dene('bos nesne', () => { const servis = degismisServisKur({ katalog: gudukKatalog(0), bildirim: sessizBildirim, gunluk: bosNesne('gunluk'), saat: () => 1000, }); servis.oduncVer(UYE, 'K-903'); }); dene('sahte nesne', () => { const sahte = sahteNesne([], 'gonder'); const servis = degismisServisKur({ katalog: gudukKatalog(5), bildirim: sahte, gunluk: sessizGunluk, saat: () => 1000, }); assert.throws(() => servis.oduncVer(UYE, 'K-903')); sahte.dogrula(); });
guduk sessiz bos nesne gunluk.yaz cagrilmamaliydi sahte nesne beklenti karsilanmadi: beklenen [], gercek [["gonder","U-42","odunc reddedildi"]]
Üç satır, tür seçiminin ne anlama geldiğini tek başına anlatır.
Güdük sessiz kaldı, çünkü dönüş değeri ve fırlatılan hata değişmemişti. Bu bir kusur değildir: güdük yalnız girdi sağlar, doğrulama yapmaz. Dönüş değerine bakan bir test yeni yan etkileri görmez ve görmemelidir.
Boş nesne konuştu, çünkü çağrılmaması gereken bir bağımlılık çağrıldı. Bu türün tek işlevi budur ve karşılığında testin dayandığı varsayımı belge hâline getirir.
Sahte nesne de konuştu, çünkü beklentisi “hiç bildirim gitmesin” idi ve bir bildirim gitti. Aynı sinyali casus nesne de verirdi; fark, doğrulamanın nerede durduğudur.
Buradan çıkan seçim kuralı üç maddedir. Doğrulanacak şey dönüş değeri ya da fırlatılan hata ise güdük yeter. Yan etkinin gerçekleşmesi doğrulanacaksa casus ya da sahte nesne gerekir. Çok adımlı bir senaryo kurulacaksa taklit nesne gerekir; güdükle kurulan çok adımlı senaryolar hızla tutarsız hâle gelir.
Çağrı doğrulaması bir maliyet taşır: test, sınanan kodun nasıl çalıştığına bağlanır. Bir yeniden düzenleme bildirim gönderimini başka bir katmana taşırsa casus ve sahte nesneyle yazılmış testler düşer, oysa dışarıdan gözlenen davranış aynıdır. Sekizinci ders bu bedeli ölçer.
Koşucunun Sağladığı Yetenek
Yerleşik test koşucusu, çağrı kaydı tutan işlevleri elle yazmak zorunda kalmamak için bir yetenek sağlar. Elle yazılan casus nesnenin yaptığı işi aynı yapar; kayıt biçimi hazır gelir.
// yerlesik.test.mjs — kosucunun sagladigi sahte islev yetenegi import { test, mock } from 'node:test'; import assert from 'node:assert/strict'; import { oduncServisiKur } from './servis.mjs'; import { bosNesne, taklitKatalog } from './sahteler.mjs'; test('yerlesik sahte islev cagri kaydini tutar', () => { const gonder = mock.fn(); const servis = oduncServisiKur({ katalog: taklitKatalog(), bildirim: { gonder }, gunluk: bosNesne('gunluk'), saat: () => 1000, }); servis.oduncVer({ no: 'U-17', tur: 'ogrenci' }, 'K-903'); assert.equal(gonder.mock.callCount(), 1); assert.deepEqual(gonder.mock.calls[0].arguments, ['U-17', 'son tarih: 1028']); });
node --test --test-reporter=tap yerlesik.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - yerlesik sahte islev cagri kaydini tutar # tests 1 # pass 1 # fail 0
Hazır yetenek yazım yükünü azaltır, ama tür seçimini yapmaz. mock.fn() çağrı kaydı
tuttuğu için casus nesne olarak da sahte nesne olarak da kullanılabilir; hangisi olduğunu
belirleyen şey testin onu nasıl doğruladığıdır. Türü seçen karar hâlâ yazan kişinindir.
Özet
- Sahte bağımlılıkların farkı yapıda değil amaçtadır; beş tür beş ayrı doğrulama sorusunu yanıtlar.
- Boş nesne yalnız yer doldurur, güdük girdi sabitler, taklit nesne çalışan bir kopyadır, casus nesne çağrıları kaydeder, sahte nesne beklentiyi kendi taşır.
- Servise iki yan etki eklendiğinde güdük sessiz kaldı, kukla nesne beklenmedik çağrıyı, sahte nesne karşılanmayan beklentiyi bildirdi.
- Dönüş değeri sınanacaksa güdük yeter; yan etki sınanacaksa casus ya da sahte nesne, çok adımlı senaryo kurulacaksa taklit nesne gerekir.
- Çağrı doğrulaması testi kodun nasıl çalıştığına bağlar; koşucunun sağladığı hazır yetenek yazım yükünü azaltır ama tür seçimini yapmaz.
Sonraki Adım
Bu derste testlerin neyi sınadığı tür tür ayrıldı, ama takımın ne kadarını sınadığı sorusu hiç sorulmadı. Bu soruya verilen yaygın yanıt bir yüzdedir: kodun kaçta kaçı test sırasında çalıştı? Sonraki ders bu yüzdenin üç ayrı biçimde hesaplandığını gösterir ve üçünü de elle sayar. Yüzde yüz satır kapsamına ulaşmış bir takımın kaçırdığı hata, aynı derste koşturularak gösterilir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.