Ders 13 / 13
Eski Kodu Test Altına Alma
Dikişsiz bir işlevin mevcut davranışının karakterizasyon testleriyle kayda geçirilmesi, parametre dikişi açılarak değiştirilebilir hâle getirilmesi ve aynı testlerin dikişten sonra da yeşil kalması.
İçindekiler
Bu konuda kurulan her şey boş bir sayfadan başladı: test önce yazıldı, kod sonra geldi. Var olan bir kod tabanında bu sıra kurulamaz. Yıllardır çalışan bir işlev zaten oradadır, davranışı yazılı değildir ve çoğu zaman kimse tam olarak ne yaptığını bilmez.
Böyle bir işlevi değiştirmenin sırası tersinedir: önce ne yaptığı kayda geçirilir, sonra değiştirilebilir hâle getirilir. Birinci adımın aracı karakterizasyon testleridir, ikincisininki dikiştir.
Dikişsiz Kod
Aşağıdaki işlev gecikme raporu üretir. Kayıtları modül düzeyinde tutar, günü sistem saatinden okur ve sonucu doğrudan çıktı akışına yazar.
// eski-rapor.mjs — yillardir calisan, testi olmayan surum const KAYITLAR = [ { kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1010, sube: 'merkez', iadeSubesi: 'merkez', iadeGunu: 1020 }, { kitapNo: 'K-101', uyeNo: 'U-42', sonGun: 1024, sube: 'merkez', iadeSubesi: 'yamac', iadeGunu: 1024 }, { kitapNo: 'K-555', uyeNo: 'U-58', sonGun: 1000, sube: 'yamac', iadeSubesi: 'yamac', iadeGunu: 1100 }, ]; export function gecikmeRaporuYaz() { const bugun = Math.floor(Date.now() / 86400000); let toplam = 0; console.log(`gecikme raporu — gun ${bugun}`); for (const kayit of KAYITLAR) { const gecikme = kayit.iadeGunu - kayit.sonGun; let ucret = 0; if (gecikme > 3) ucret = (gecikme - 3) * 2; if (kayit.iadeSubesi !== kayit.sube) ucret += 5; if (ucret > 40) ucret = 40; toplam += ucret; console.log(`${kayit.kitapNo} ${kayit.uyeNo} gecikme ${gecikme} ucret ${ucret}`); } console.log(`toplam ${toplam}`); }
Bir kod parçasında dikiş (seam), davranışı kaynağı değiştirilmeden başkalaştırılabilen noktadır. Bu işlevde öyle bir nokta yoktur. Girdisi yoktur — kayıtlar içeridedir. Zamanı dışarıdan alamaz — saati kendisi okur. Sonucu döndürmez — yazar. Beşinci dersteki ölçüm tam olarak bu üç bağımlılığı adlandırmıştı; burada üçü aynı işlevde birleşmiştir.
Yine de bir gözlem noktası vardır: yazdığı satırlar. Küresel nesneler değiştirilerek hem saat sabitlenebilir hem yazılanlar toplanabilir. Beşinci derste bu yolun pahalı olduğu söylenmişti — burada geçici olarak tek yol odur.
// gozlem.mjs — eski islevin bugunku ciktisi kayda geciriliyor import { gecikmeRaporuYaz } from './eski-rapor.mjs'; const gercekNow = Date.now; Date.now = () => 1020 * 86400000; try { gecikmeRaporuYaz(); } finally { Date.now = gercekNow; }
gecikme raporu — gun 1020 K-903 U-17 gecikme 10 ucret 14 K-101 U-42 gecikme 0 ucret 5 K-555 U-58 gecikme 100 ucret 40 toplam 59
İkinci satırda dikkat çeken bir şey var: K-101 kaydının gecikmesi sıfır, ücreti beş. Yedinci derste bunun bir hata olduğu belirlenmişti — zamanında iade edilen kitaba şube dışı ücreti uygulanıyor. Karakterizasyon testinin buradaki tavrı belirleyicidir.
Karakterizasyon Testleri
Karakterizasyon testi (characterization test), kodun ne yapması gerektiğini değil, şu anda ne yaptığını kayda geçiren testtir. Beklenen değeri belirtimden almaz; koşumdan alır. Amacı doğrulamak değil, ileride yapılacak değişikliğin fark edilmesini sağlamaktır.
Bu, birinci derste konulan kuralın kasıtlı bir istisnasıdır. Orada beklenen değerin sınanan koddan bağımsız bir kaynaktan gelmesi gerektiği söylenmişti; burada beklenen değer doğrudan sınanan koddan gelir. Fark amaçtadır: bu testler doğruluk iddiası taşımaz, bir taban çizgisi kurar.
// karakterizasyon.test.mjs — mevcut davranisi oldugu gibi kayda geciren testler import { test } from 'node:test'; import assert from 'node:assert/strict'; import { gecikmeRaporuYaz } from './eski-rapor.mjs'; function yakala(bugun) { const gercekNow = Date.now; const gercekLog = console.log; const satirlar = []; Date.now = () => bugun * 86400000; console.log = (satir) => satirlar.push(satir); try { gecikmeRaporuYaz(); } finally { Date.now = gercekNow; console.log = gercekLog; } return satirlar; } test('rapor basligi bugunun gun numarasini tasir', () => { assert.equal(yakala(1020)[0], 'gecikme raporu — gun 1020'); }); test('rapor satirlari bugunku haliyle uretilir', () => { assert.deepEqual(yakala(1020).slice(1), [ 'K-903 U-17 gecikme 10 ucret 14', 'K-101 U-42 gecikme 0 ucret 5', 'K-555 U-58 gecikme 100 ucret 40', 'toplam 59', ]); }); test('rapor icerigi bugunun gun numarasindan etkilenmez', () => { assert.deepEqual(yakala(2000).slice(1), yakala(1020).slice(1)); });
node --test --test-reporter=tap karakterizasyon.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - rapor basligi bugunun gun numarasini tasir ok 2 - rapor satirlari bugunku haliyle uretilir ok 3 - rapor icerigi bugunun gun numarasindan etkilenmez # tests 3 # pass 3 # fail 0
İkinci test, bilinen hatayı da kayda geçiriyor: K-101 U-42 gecikme 0 ucret 5 satırı
beklenen sonucun içinde. Bu bilerek yapılır. Karakterizasyon aşamasında hata düzeltilirse
iki değişiklik aynı anda yapılmış olur — yapı değişikliği ile davranış değişikliği
birbirine karışır ve düşen bir test hangisinden geldiğini söyleyemez.
Üçüncü test bir belirsizliği kapatıyor: rapor içeriğinin bugüne bağlı olmadığı, iki farklı gün numarasıyla alınan sonuçların karşılaştırılmasıyla kayda geçiyor. Karakterizasyon testleri yazılırken bu tür sorular sistematik olarak sorulur, çünkü bilinmeyen bağımlılık sonraki adımı bozan şeydir.
Dikiş Açmak
Taban çizgisi kurulduktan sonra yapı değiştirilebilir. En ucuz dikiş türü parametre dikişidir: içeriden okunan değer bir parametreye taşınır ve eski çağıranlar için o parametreyi dolduran ince bir sarmalayıcı bırakılır.
// eski-rapor.mjs — surum 2: parametre dikisi acildi, davranis aynen korundu const KAYITLAR = [ { kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1010, sube: 'merkez', iadeSubesi: 'merkez', iadeGunu: 1020 }, { kitapNo: 'K-101', uyeNo: 'U-42', sonGun: 1024, sube: 'merkez', iadeSubesi: 'yamac', iadeGunu: 1024 }, { kitapNo: 'K-555', uyeNo: 'U-58', sonGun: 1000, sube: 'yamac', iadeSubesi: 'yamac', iadeGunu: 1100 }, ]; export function raporSatirlari(kayitlar, bugun) { const satirlar = [`gecikme raporu — gun ${bugun}`]; let toplam = 0; for (const kayit of kayitlar) { const gecikme = kayit.iadeGunu - kayit.sonGun; let ucret = 0; if (gecikme > 3) ucret = (gecikme - 3) * 2; if (kayit.iadeSubesi !== kayit.sube) ucret += 5; if (ucret > 40) ucret = 40; toplam += ucret; satirlar.push(`${kayit.kitapNo} ${kayit.uyeNo} gecikme ${gecikme} ucret ${ucret}`); } satirlar.push(`toplam ${toplam}`); return satirlar; } export function gecikmeRaporuYaz() { const bugun = Math.floor(Date.now() / 86400000); for (const satir of raporSatirlari(KAYITLAR, bugun)) console.log(satir); }
Üç şey aynı anda yapıldı: kayıtlar ve gün parametreye taşındı, çıktı yazma işi hesaptan ayrıldı ve eski dış arayüz aynen korundu. Bu bir yeniden düzenlemedir — davranış değişmemelidir. Doğrulaması, hiç değiştirilmemiş karakterizasyon testlerinin yeniden koşturulmasıdır.
node --test --test-reporter=tap karakterizasyon.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - rapor basligi bugunun gun numarasini tasir ok 2 - rapor satirlari bugunku haliyle uretilir ok 3 - rapor icerigi bugunun gun numarasindan etkilenmez # tests 3 # pass 3 # fail 0
Parametre dikişi tek seçenek değildir. Nesne dikişi, bağımlılığı bir nesne alanı olarak verir — beşinci dersteki servis kurulumu bunun örneğidir. Modül dikişi, çağrılan modülün kendisini değiştirilebilir kılar. Üçünün ortak yanı, davranışın kaynak değiştirmeden başkalaştırılabildiği bir nokta yaratmalarıdır; farkı maliyet ve okunabilirliktedir.
Dikişten Geçen Testler
Dikiş açıldıktan sonra yazılan testler küresel nesnelere dokunmaz. Saat parametredir, kayıtlar parametredir, sonuç dönüş değeridir.
// dikis.test.mjs — dikisten gecen testler: saat ve kayitlar disaridan veriliyor import { test } from 'node:test'; import assert from 'node:assert/strict'; import { raporSatirlari } from './eski-rapor.mjs'; const kayit = (ek) => ({ kitapNo: 'K-903', uyeNo: 'U-17', sonGun: 1010, sube: 'merkez', iadeSubesi: 'merkez', ...ek, }); test('ucretsiz gun payi icindeki gecikme ucretlendirilmez', () => { const satirlar = raporSatirlari([kayit({ iadeGunu: 1013 })], 1020); assert.equal(satirlar[1], 'K-903 U-17 gecikme 3 ucret 0'); }); test('gecikmesiz sube disi iade bes birim ucretlendirilir', () => { const satirlar = raporSatirlari([kayit({ iadeGunu: 1010, iadeSubesi: 'yamac' })], 1020); assert.equal(satirlar[1], 'K-903 U-17 gecikme 0 ucret 5'); }); test('bos kayit listesi toplami sifir verir', () => { assert.deepEqual(raporSatirlari([], 1020), ['gecikme raporu — gun 1020', 'toplam 0']); });
node --test --test-reporter=tap dikis.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - ucretsiz gun payi icindeki gecikme ucretlendirilmez ok 2 - gecikmesiz sube disi iade bes birim ucretlendirilir ok 3 - bos kayit listesi toplami sifir verir # tests 3 # pass 3 # fail 0
İkinci testin adı bilinen hatayı açıkça adlandırıyor. Bu, karakterizasyon aşamasından kalan mirasın son hâlidir: davranış artık gizli bir yan etki değil, adı konmuş ve sınanan bir kural. Düzeltme kararı verildiğinde yapılacak iş bellidir — testin adı ve beklentisi değişir, sonra gerçekleştirim; ve bu artık üçüncü dersteki sıradan kırmızı–yeşil turudur.
Sıra da bu yüzden önemlidir. Karakterizasyon testleri olmadan yapılan bir dikiş açma işleminde, davranışın korunup korunmadığını söyleyen hiçbir şey yoktur. Testler önce yazılır, yapı sonra değişir, düzeltme en sonda gelir.
Özet
- Dikiş, bir kodun davranışının kaynağı değiştirilmeden başkalaştırılabildiği noktadır; dikişsiz kodda tek gözlem noktası küresel nesneler olabilir.
- Karakterizasyon testi kodun ne yapması gerektiğini değil şu anda ne yaptığını kayda geçirir; beklenen değeri belirtimden değil koşumdan alır.
- Bilinen hatalar da taban çizgisine dahil edilir; düzeltme ile yapı değişikliği aynı anda yapılmaz.
- Parametre dikişi içeriden okunan değeri parametreye taşır ve eski arayüzü ince bir sarmalayıcıyla korur; nesne dikişi ve modül dikişi aynı amacın başka maliyetli biçimleridir.
- Dikişten geçen testler küresel nesnelere dokunmaz; sıra her zaman karakterizasyon, dikiş, düzeltme biçimindedir.
Kurs Kapanışı
Bu kurs iki konuda tek bir soruyu iki yönden ele aldı: bir birim testi neyi kanıtlar ve o kanıt nasıl güvenilir tutulur?
Birim Test Pratiği konusu testi bir ölçüm aracına çevirdi. Testin gövdesi hazırla, uygula ve doğrula bölümlerine ayrıldı; iddianın sinyal gücü yakaladığı bozulma sayısıyla, adlandırmanın değeri düşen koşumun taşıdığı bilgiyle, bağımsızlık sıra permütasyonuyla, düzenek maliyeti satır sayısıyla, kapsam üç ayrı birimin elle sayımıyla ve kırılganlık yeniden düzenleme karşısında düşen test sayısıyla ölçüldü. Sahte bağımlılıkların beş türü elle yazıldı ve her birinin yanıtladığı soru ayrıldı.
Test Odaklı Geliştirme konusu sırayı tersine çevirdi. Kırmızı–yeşil–yeniden düzenle döngüsü adım adım koşturuldu, adım boyunun geri bildirim süresine etkisi arama alanıyla ölçüldü, kurallar alan diliyle yazılmış örnek tablolarına taşındı, kabul ölçütleri dış halka olarak kuruldu ve son olarak testi olmayan bir kod karakterizasyon testleriyle taban çizgisine oturtulup dikişle açıldı.
Kursun boyunca sessizce taşınan bir varsayım var ve adının konması gerekiyor. Buradaki her test süreç içinde ve gerçek bağımlılık olmadan koştu. Katalog bir eşlemeydi, depo bir taklit nesneydi, bildirim bir casus nesneydi, saat bir işlevdi. Bu sahte bağımlılıkların gerçeğine uyduğu varsayıldı — ama hiçbir yerde sınanmadı. Taklit katalog gerçek veri tabanının kısıtlarını taşımıyor; casus bildirim, gerçek bildirim kanalının başarısızlıklarını üretmiyor; sürücünün kurduğu dünya, gerçek bir dağıtımda karşılaşılacak sınırların hiçbirini içermiyor.
Bu varsayımın sınanması ayrı bir test katmanının konusudur. Tümleşim, Sözleşme ve Uçtan Uca Test kursu bu sınırları gerçek çalıştırarak ele alır: hangi bağımlılığın gerçeği kullanılır, sahte bağımlılık ile gerçeğin aynı sözleşmeye uyduğu nasıl güvenceye alınır ve uygulamanın tamamı üzerinden koşan testler nasıl kararlı tutulur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.