Ders 21 / 27
Birim ve Bileşen Testleri
Bileşen testinin birim testinden ayrıldığı sınır, davranış sözleşmesi ile uygulama ayrıntısının ayrılması ve aynı yeniden düzenlemede iki test biçiminin nasıl farklı davrandığı.
İçindekiler
Güvenlik konusu bir savunma katmanı dizisiyle kapandı: çıktı kaçırma, içerik güvenlik politikası, kaynak kısıtları, istek sahteciliğine karşı belirteçler, güvenlik başlıkları ve üçüncü taraf betik denetimi. Başarım konusu bir bütçe bıraktı, erişilebilirlik konusu bir ölçüt kümesi. Bu kararların hepsi aynı soruyu sorar: kurulan şey bozulduğunda bunu ne haber verecek?
Elle denetim bu soruya yanıt vermez. Bir savunmanın kaldırıldığını, bir rolün düştüğünü ya da bir bütçenin aşıldığını fark etmek için o değişikliği gören bir mekanizma gerekir. Bu konu, o mekanizmayı iki yönden kurar: değişiklikten önce çalışan testler ve yayımdan sonra çalışan izleme. Konunun ilk sorusu en dar olanıdır — bir bileşenin doğru çalıştığı nasıl sınanır?
Sınanan Sınır
Test türleri ne sınadıklarıyla değil, nereye kadar uzandıklarıyla ayrılır.
Birim testi (unit test) tek bir fonksiyonu ya da modülü, dış dünyaya dokunmadan sınar. Süzgeç ölçütünü ölçüm listesine uygulayan saf fonksiyon bu kapsama girer. Node.js Çalışma Zamanı kursundaki Test dersinde kurulan biçim aynen geçerlidir.
Bileşen testi (component test) bir bileşeni özellikleriyle kurar, ona kullanıcı olayı gönderir ve çizilen sonucu inceler. Sınırı bileşenin kendi ağacıdır: dışarıdaki ağ, adres çubuğu ve depolama bu sınırın ötesindedir.
Tümleşim testi (integration test) birkaç bileşeni ya da bir bileşenle veri katmanını birlikte sınar. Uçtan uca test (end-to-end test) ise uygulamayı gerçek bir tarayıcıda çalıştırır; üçüncü ders onu ele alır.
Bileşen testinin ayırt edici yanı, girdisinin de çıktısının da kullanıcı sözlüğünde olmasıdır: girdi bir olay, çıktı ekranda görünen şeydir. Testin bileşenin içine bakması gerekmez — ve bakmaması, bu dersin asıl konusudur.
Küçük Bir Bileşen Modeli
Bileşen sınamanın mantığını görmek için bir çatıya gerek yoktur. Aşağıdaki yirmi beş satır bir bileşen modelinin özünü verir: durum, o durumdan üretilen ağaç ve durumu ilerleten olay.
// bilesen-modeli.mjs — kucuk bir bilesen modeli: durum, cizim, olay export function o(tur, ozellikler = {}, ...cocuklar) { return { tur, ozellikler, cocuklar: cocuklar.flat() }; } export function bagla(bilesen) { let durum = bilesen.baslangic(); const ornek = { durum: () => durum, agac: () => bilesen.ciz(durum), gonder(olay) { durum = bilesen.indirge(durum, olay); return ornek; }, }; return ornek; } export function* dolas(dugum) { if (dugum === null || typeof dugum !== 'object') return; yield dugum; for (const cocuk of dugum.cocuklar ?? []) yield* dolas(cocuk); } export function metin(dugum) { if (typeof dugum === 'string') return dugum; return (dugum.cocuklar ?? []).map(metin).join(' ').trim(); }
bagla bir bileşeni sınama için ayağa kaldırır: durumu tutar, olay geldiğinde indirgeyiciyi
çağırır ve her istendiğinde güncel ağacı üretir. Bileşen Tabanlı Geliştirme kursundaki
bildirimsel model burada en yalın hâlindedir; ağacın gerçekten belgeye yazılması sınama için
gerekli değildir.
Sınanacak bileşen, Kuzey Yamaç arayüzünün ölçüm tablosudur: bir süzgeç alanı ve eşleşen ölçümleri gösteren bir tablo.
// olcum-tablosu.mjs — surum 1 import { o } from './bilesen-modeli.mjs'; const OLCUMLER = [ { istasyon: 'kuzey-yamac-01', deger: 21.4 }, { istasyon: 'kuzey-yamac-02', deger: 19.8 }, { istasyon: 'dogu-sirt-01', deger: 24.1 }, { istasyon: 'vadi-tabani-03', deger: 17.2 }, ]; export const OlcumTablosu = { baslangic: () => ({ suzgecMetni: '', olcumler: OLCUMLER }), indirge(durum, olay) { if (olay.tur === 'suzgec-degisti') return { ...durum, suzgecMetni: olay.deger }; return durum; }, ciz(durum) { const gorunen = durum.olcumler.filter((x) => x.istasyon.includes(durum.suzgecMetni)); return o('section', { class: 'olcum-tablosu' }, o('label', { for: 'suzgec' }, 'Istasyon suzgeci'), o('input', { id: 'suzgec', type: 'search', value: durum.suzgecMetni }), o('table', { class: 'tablo' }, ...gorunen.map((x) => o('tr', { class: 'satir' }, o('td', {}, x.istasyon), o('td', {}, x.deger.toFixed(1)))), ), o('p', { class: 'ozet' }, `${gorunen.length} olcum`), ); }, };
İki Test Biçimi
Aynı bileşen iki farklı biçimde sınanabilir. Birinci biçim bileşenin içine bakar: durum nesnesinin alan adına ve ağaçtaki çocuk sırasına dayanır. İkinci biçim yalnız dışarıdan görüneni doğrular.
// olcum-tablosu.test.mjs import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { bagla, dolas, metin } from './bilesen-modeli.mjs'; import { OlcumTablosu } from './olcum-tablosu.mjs'; function satirMetinleri(agac) { return [...dolas(agac)].filter((d) => d.tur === 'tr').map(metin); } describe('uygulama ayrintisina bagli', () => { test('suzgec durumu yazilan degeri tutar', () => { const ornek = bagla(OlcumTablosu); ornek.gonder({ tur: 'suzgec-degisti', deger: 'kuzey' }); assert.equal(ornek.durum().suzgecMetni, 'kuzey'); }); test('ikinci cocuk girdi ogesidir', () => { const ornek = bagla(OlcumTablosu); assert.equal(ornek.agac().cocuklar[1].tur, 'input'); }); }); describe('davranisa bagli', () => { test('suzgec yazildiginda yalniz eslesen istasyonlar kalir', () => { const ornek = bagla(OlcumTablosu); ornek.gonder({ tur: 'suzgec-degisti', deger: 'kuzey' }); assert.deepEqual(satirMetinleri(ornek.agac()), [ 'kuzey-yamac-01 21.4', 'kuzey-yamac-02 19.8', ]); }); test('eslesme yoksa tablo bosalir ve ozet sifir der', () => { const ornek = bagla(OlcumTablosu); ornek.gonder({ tur: 'suzgec-degisti', deger: 'zirve' }); assert.deepEqual(satirMetinleri(ornek.agac()), []); assert.match(metin(ornek.agac()), /0 olcum/); }); });
Üç dosya aynı dizinde durur ve test koşucusu çalışma zamanının kendi parçasıdır:
node --test olcum-tablosu.test.mjs
▶ uygulama ayrintisina bagli ✔ suzgec durumu yazilan degeri tutar (0.260292ms) ✔ ikinci cocuk girdi ogesidir (0.096083ms) ✔ uygulama ayrintisina bagli (0.96575ms) ▶ davranisa bagli ✔ suzgec yazildiginda yalniz eslesen istasyonlar kalir (0.350458ms) ✔ eslesme yoksa tablo bosalir ve ozet sifir der (0.087291ms) ✔ davranisa bagli (0.50475ms) ℹ tests 4 ℹ suites 2 ℹ pass 4 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 35.797459
Süre alanları ortama bağlıdır ve her koşumda değişir; anlamlı olan sayaçlardır. Dördü de geçiyor. Bu noktada iki biçim arasında bir fark görünmüyor.
Yeniden Düzenleme Sınavı
Fark, kod değiştiğinde ortaya çıkar. Aşağıdaki sürüm iki değişiklik yapıyor: durum alanının
adı suzgecMetni yerine sorgu oldu ve süzgeç alanı ile etiketi bir kapsayıcının içine
alındı. Süzme mantığı ayrı bir fonksiyona taşındı. Kullanıcının gördüğü hiçbir şey
değişmedi.
// olcum-tablosu.mjs — surum 2: alan adi degisti, suzgec paneli sarmalandi import { o } from './bilesen-modeli.mjs'; const OLCUMLER = [ { istasyon: 'kuzey-yamac-01', deger: 21.4 }, { istasyon: 'kuzey-yamac-02', deger: 19.8 }, { istasyon: 'dogu-sirt-01', deger: 24.1 }, { istasyon: 'vadi-tabani-03', deger: 17.2 }, ]; function suz(olcumler, sorgu) { return olcumler.filter((x) => x.istasyon.includes(sorgu)); } export const OlcumTablosu = { baslangic: () => ({ sorgu: '', olcumler: OLCUMLER }), indirge(durum, olay) { if (olay.tur === 'suzgec-degisti') return { ...durum, sorgu: olay.deger }; return durum; }, ciz(durum) { const gorunen = suz(durum.olcumler, durum.sorgu); return o('section', { class: 'olcum-tablosu' }, o('div', { class: 'suzgec-paneli' }, o('label', { for: 'suzgec' }, 'Istasyon suzgeci'), o('input', { id: 'suzgec', type: 'search', value: durum.sorgu })), o('table', { class: 'tablo' }, ...gorunen.map((x) => o('tr', { class: 'satir' }, o('td', {}, x.istasyon), o('td', {}, x.deger.toFixed(1)))), ), o('p', { class: 'ozet' }, `${gorunen.length} olcum`), ); }, };
Test dosyası değişmeden aynı komutla yeniden çalıştırılır. Aşağıdaki çıktıdan sayaç satırlarının bir bölümü ve başarısızlık gövdelerindeki yığıt satırları çıkarılmıştır.
▶ uygulama ayrintisina bagli ✖ suzgec durumu yazilan degeri tutar (0.727333ms) ✖ ikinci cocuk girdi ogesidir (0.515ms) ✖ uygulama ayrintisina bagli (1.638333ms) ▶ davranisa bagli ✔ suzgec yazildiginda yalniz eslesen istasyonlar kalir (0.3995ms) ✔ eslesme yoksa tablo bosalir ve ozet sifir der (0.103625ms) ✔ davranisa bagli (0.589375ms) ℹ tests 4 ℹ pass 2 ℹ fail 2 ✖ suzgec durumu yazilan degeri tutar AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + actual - expected + undefined - 'kuzey' ✖ ikinci cocuk girdi ogesidir AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 'table' !== 'input'
İki test düştü ve ikisi de gerçek bir kusuru bildirmiyor. Bildirdikleri şey, kodun yeniden düzenlendiğidir. Bu, testin verdiği en pahalı yanlış işarettir: değişiklik güvenli olduğu hâlde kırmızı, dolayısıyla her yeniden düzenleme bir de test bakımı getirir. Bir süre sonra ekip testleri düzeltmek yerine yeniden düzenlemekten vazgeçer.
Davranışa bağlı iki test, aynı değişiklikte sessiz kaldı — çünkü sınadıkları şey değişmemişti. Ayrım şudur: bileşenin davranış sözleşmesi kullanıcının gözlemleyebildiği şeydir. Ekranda görünen metinler, erişilebilirlik ağacındaki roller, tetiklenen dış çağrılar. Uygulama ayrıntısı ise sözleşmeyi yerine getirmek için seçilen yoldur: durum alanının adı, ağaçtaki sarmalayıcılar, yardımcı fonksiyonların bölünmesi.
Bir kural olarak: test, bileşeni yazan kişinin değiştirmekte özgür olması gereken hiçbir şeye dayanmamalıdır.
Sınırın Ötesini Değiştirmek
Bileşen testinin dışarıya açılan yerleri de vardır: ağ isteği, saat, rastgelelik, depolama. Bunlar sınanan sınırın ötesindedir ve testte yerlerine sahte bağımlılık (test double) konur. İki biçimi ayırmak yeterlidir: güdük (stub) sabit bir yanıt döndürür, sahte nesne (mock) ek olarak kendisine yapılan çağrıları kaydeder ve test bu kayıtları doğrular.
Node.js kursundaki Test dersinde kurulan yöntem burada da geçerlidir: bağımlılık dışarıdan verilir. Bileşen saatı kendi içinden okumak yerine özellik olarak alırsa, test sabit bir saat verir ve çıktı belirlenimci olur. Aynı kural ağ için de işler — ölçüm listesini getiren fonksiyon dışarıdan verildiğinde bileşen testi ağa hiç dokunmaz.
Sahte nesnenin bir maliyeti vardır: doğruladığı şey gerçek bağımlılığın davranışı değil, o davranışa dair varsayımdır. Varsayım kayarsa test yeşil kalırken uygulama bozulur. Bu yüzden sahte nesne sayısı azaldıkça testin değeri artar; sınırı olabildiğince dışarı itmek, tümleşim testinin varlık nedenidir.
Özet
- Test türleri sınadıkları şeyle değil, uzandıkları sınırla ayrılır; bileşen testinin sınırı bileşenin kendi ağacıdır.
- Bileşen testinin girdisi bir olay, çıktısı ekranda görünendir; ikisi de kullanıcı sözlüğündedir.
- Davranış sözleşmesi gözlemlenebilir olandır; uygulama ayrıntısı onu yerine getiren yoldur. Test yalnız birincisine dayanmalıdır.
- Alan adı ve ağaç yapısı değiştiren bir yeniden düzenlemede uygulama ayrıntısına bağlı iki test düştü, davranışa bağlı iki test geçti; düşenler gerçek bir kusur bildirmiyordu.
- Ağ, saat ve rastgelelik gibi sınır ötesi bağımlılıklar dışarıdan verilir; güdük sabit yanıt döndürür, sahte nesne çağrıları da kaydeder.
Sonraki Adım
Davranışa bağlı test yazmak bir soruyu açıkta bırakıyor: sınanacak eleman testin içinde nasıl bulunacak? Bu dersteki yardımcı fonksiyon düğüm türüne göre arama yaptı, yani hâlâ bir uygulama ayrıntısına — ağacın etiket seçimine — dayanıyordu. Sınıf adına dayanan bir seçici de aynı sorunu taşır. Kullanıcının elemanı tanıma biçimi bunların hiçbiri değildir: kullanıcı düğmeyi düğme olduğu ve üstünde “Kaydet” yazdığı için bulur. Sonraki ders bu iki bilgiyi — erişilebilir rolü ve erişilebilir adı — sorgu ölçütüne dönüştürür.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.