Ders 16 / 16
Test Altında Yeniden Düzenleme
Testsiz yapılan bir ayıklama adımında sessizce değişen davranışın tarama ile bulunması, altı testlik kümenin yakalama gücünün bozma denemeleriyle ölçülmesi ve güçlendirilmiş kümenin aynı adımı kırmızıya çevirmesi.
İçindekiler
Önceki derste her adımın ardından alınan yeşil koşum, davranışın korunduğunun kanıtı sayıldı. Bu sayışın kendisi sınanmadı: altı test, ücret hesabının bütün davranışını gerçekten kapsıyor mu? Bu ders önce testsiz yapılan bir ayıklama adımında sessizce değişen bir davranışı gösterir, aynı adımın mevcut altı testle de yakalanmadığını ölçer, test kümesinin yakalama gücünü sayıya döker ve güçlendirilmiş kümenin aynı adımı kırmızıya çevirdiği anı gösterir.
Testlerin buradaki işi tektir: davranış koruması. Test yazımı, sahte bağımlılık kurma ve testi tasarımın sürükleyicisi yapma, Yazılım Kalitesi ve Test müfredatının konularıdır. Burada testlere yalnız tek bir soru sorulur — yeniden düzenlemeden önceki ve sonraki kod aynı girdilere aynı yanıtı veriyor mu?
Başlangıç noktası önceki dersin bitiş durumudur.
// tarife.mjs — onceki dersin bitis hali export const KADEME = [ { ustGram: 1000, kurus: 4990, ad: 'kucuk' }, { ustGram: 5000, kurus: 7490, ad: 'orta' }, { ustGram: 10000, kurus: 9900, ad: 'buyuk' }, { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' }, { ustGram: Infinity, kurus: 24900, ad: 'agir' }, ]; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; export function agirlikKademesi(gram) { return KADEME.find((k) => gram <= k.ustGram); } export function bolgeKatsayisi(bolge) { return BOLGE_KATSAYI[bolge]; }
// ucret.mjs — onceki dersin bitis hali import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs'; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(gonderi) { const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3); return Math.max(gonderi.gram, hacim); } export function indirimOrani(adet) { return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1; } export function gonderiUcreti(gonderi) { const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus; return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); return Math.round(toplam * indirimOrani(gonderiler.length)); }
// ucret.test.mjs — onceki dersten devralinan alti test import { test } from 'node:test'; import assert from 'node:assert/strict'; import { gonderiUcreti, donemUcreti } from './ucret.mjs'; const kutu = { en: 20, boy: 15, yukseklik: 10 }; const donem = (adet) => Array.from({ length: adet }, () => ({ ...kutu, gram: 3000, bolge: 'B1' })); test('kademe ucreti agirliktan okunur', () => { assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490); }); test('bolge katsayisi taban ucreti carpar', () => { assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984); }); test('asgari ucret bolge carpimindan sonra uygulanir', () => { assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990); }); test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => { assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125); }); test('esigi asan donemde butun gonderiler indirimli toplanir', () => { assert.equal(donemUcreti(donem(60)), Math.round(60 * 7490 * 0.88)); }); test('esigin altindaki donemde indirim uygulanmaz', () => { assert.equal(donemUcreti(donem(10)), 10 * 7490); });
Karşılaştırma yapılabilmesi için düzenlemeden önceki sürümün bir kopyası saklanıyor.
cp ucret.mjs ucret-onceki.mjs node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
# tests 6 # pass 6 # fail 0
Sessizce Değişen Davranış
Düzenleme, önceki dersteki ayıklamanın aynısıdır: dönem indiriminin eşik kararı kendi adını alıyor. Adı verilirken karşılaştırmanın yönü kayıyor.
// ucret.mjs — ayiklama: esik karari indirimliMi adiyla cikarildi import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs'; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(gonderi) { const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3); return Math.max(gonderi.gram, hacim); } export function indirimliMi(adet) { return adet >= INDIRIM_ESIGI; } export function indirimOrani(adet) { return indirimliMi(adet) ? INDIRIM_ORANI : 1; } export function gonderiUcreti(gonderi) { const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus; return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); return Math.round(toplam * indirimOrani(gonderiler.length)); }
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|# (tests|pass|fail))'
ok 1 - kademe ucreti agirliktan okunur ok 2 - bolge katsayisi taban ucreti carpar ok 3 - asgari ucret bolge carpimindan sonra uygulanir ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir ok 5 - esigi asan donemde butun gonderiler indirimli toplanir ok 6 - esigin altindaki donemde indirim uygulanmaz # tests 6 # pass 6 # fail 0
Altı testin altısı yeşil. Testsiz bir kod tabanında bu adım fark edilmeden geçerdi; testli bir kod tabanında da fark edilmeden geçti. Değişikliğin gerçekten davranışı koruyup korumadığını görmenin yolu, iki sürümü aynı girdiler üzerinde karşılaştırmaktır.
// fark.mjs — ayni girdilerde iki surumun sonucunu karsilastirir import { donemUcreti as onceki } from './ucret-onceki.mjs'; import { donemUcreti as simdiki } from './ucret.mjs'; const kutu = { en: 20, boy: 15, yukseklik: 10 }; let denenen = 0; const ayrisan = []; for (const gram of [500, 3000, 12000]) { for (let adet = 1; adet <= 120; adet++) { const donem = Array.from({ length: adet }, () => ({ ...kutu, gram, bolge: 'B1' })); denenen += 1; const a = onceki(donem); const b = simdiki(donem); if (a !== b) ayrisan.push(`adet=${adet} gram=${gram} onceki=${a} simdiki=${b}`); } } console.log('denenen donem:', denenen); console.log('sonucu degisen:', ayrisan.length); console.log('ilk ornek:', ayrisan[0] ?? 'yok');
denenen donem: 360 sonucu degisen: 3 ilk ornek: adet=50 gram=500 onceki=349500 simdiki=307560
Üç yüz altmış dönemden üçünün sonucu değişti ve üçü de aynı noktada: tam elli gönderilik dönemler. Alan karşılığı, tam eşikte duran bir sözleşmeli müşterinin faturasının 3.495,00 TL yerine 3.075,60 TL çıkmasıdır. Yeşil koşum yanlış bir şey söylemedi; kendisine sorulmayan soruyu yanıtlamadı. Testler girdi uzayının tamamını değil, kendi girdilerini sabitler.
Korumanın Ölçülmesi
O hâlde ölçülmesi gereken büyüklük, test kümesinin yakalama gücüdür: kaç tür davranış değişikliği koşumu kırmızıya çevirir? Bu, koda kasıtlı ve küçük bozmalar uygulayıp koşumun tepkisini sayarak ölçülür. Her bozma tek bir karakter ya da tek bir sayı düzeyindedir ve ölçümden sonra geri alınır; bunlara bozma denemesi (mutation) denir.
cp ucret.mjs ucret-ayiklanmis.mjs cp ucret-onceki.mjs ucret.mjs node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
# tests 6 # pass 6 # fail 0
// koruma.mjs — test kumesinin kac davranis degisikligini yakaladigini olcer import { readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; const MUTASYON = [ ['esik karsilastirmasi', 'ucret.mjs', 'adet > INDIRIM_ESIGI', 'adet >= INDIRIM_ESIGI'], ['asgari ucret', 'ucret.mjs', 'ASGARI_KURUS = 6990', 'ASGARI_KURUS = 6890'], ['indirim orani', 'ucret.mjs', 'INDIRIM_ORANI = 0.88', 'INDIRIM_ORANI = 0.89'], ['hacim yuvarlamasi', 'ucret.mjs', 'Math.ceil(', 'Math.floor('], ['bolge katsayisi', 'tarife.mjs', 'B2: 1.25', 'B2: 1.3'], ['kademe siniri', 'tarife.mjs', 'gram <= k.ustGram', 'gram < k.ustGram'], ]; function kirmiziMi() { try { execFileSync('node', ['--test', 'ucret.test.mjs'], { stdio: 'ignore' }); return false; } catch { return true; } } let yakalanan = 0; for (const [ad, dosya, eski, yeni] of MUTASYON) { const asil = readFileSync(dosya, 'utf8'); writeFileSync(dosya, asil.replace(eski, yeni)); const kirmizi = kirmiziMi(); writeFileSync(dosya, asil); if (kirmizi) yakalanan += 1; console.log(`${ad}: ${kirmizi ? 'yakalandi' : 'kacti'}`); } console.log(`yakalanan: ${yakalanan}/${MUTASYON.length}`);
esik karsilastirmasi: kacti asgari ucret: yakalandi indirim orani: yakalandi hacim yuvarlamasi: kacti bolge katsayisi: yakalandi kademe siniri: yakalandi yakalanan: 4/6
Altı bozmadan dördü yakalandı. Kaçan ikisi tesadüfen kaçmıyor. Eşik karşılaştırması kaçtı, çünkü dönem testleri 60 ve 10 gönderi deniyor; ikisi de eşiğin kendisi değil. Hacim yuvarlaması kaçtı, çünkü testlerdeki iki kutunun hacim ağırlığı da tam sayı çıkıyor — 3.000 ve 12.000 gram — ve yukarı yuvarlama ile aşağı yuvarlama aynı sonucu veriyor. İki kaçış da aynı desende: sınır denenmemiş.
Kapatma yolu, önceki dersin ayıklamalarının açtığı kapıdan geçer. Ayıklama sınanabilir birim sayısını artırmıştı; sınır davranışı artık dış uçtan değil, o birimlerin kendisinden sabitlenebilir.
cat >> ucret.test.mjs <<'SON' import { etkinAgirlik, indirimOrani } from './ucret.mjs'; test('hacim agirligi yukari yuvarlanir', () => { assert.equal(etkinAgirlik({ en: 11, boy: 11, yukseklik: 11, gram: 100 }), 444); }); test('esikteki gonderi sayisi indirim uretmez', () => { assert.equal(indirimOrani(50), 1); }); SON node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' node koruma.mjs
# tests 8 # pass 8 # fail 0 esik karsilastirmasi: yakalandi asgari ucret: yakalandi indirim orani: yakalandi hacim yuvarlamasi: yakalandi bolge katsayisi: yakalandi kademe siniri: yakalandi yakalanan: 6/6
İki test, kaçan iki bozmayı kapattı. Kazanılan büyüklük test sayısı değil, oran: yakalama gücü altıda dörtten altıda altıya çıktı. Bu oran bir güvence belgesi değil, bir ölçüdür; listede olmayan bir bozma türü hâlâ kaçabilir. Ölçünün değeri, “testlerim yeterli mi” sorusunu sayılabilir bir soruya çevirmesindedir.
Aynı Adım, Güçlendirilmiş Küme
Kaymış ayıklama şimdi geri getiriliyor.
cp ucret-ayiklanmis.mjs ucret.mjs node --test --test-reporter=tap ucret.test.mjs \ | grep -E '^ *(ok|not ok|expected:|actual:|# (tests|pass|fail))'
ok 1 - kademe ucreti agirliktan okunur ok 2 - bolge katsayisi taban ucreti carpar ok 3 - asgari ucret bolge carpimindan sonra uygulanir ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir ok 5 - esigi asan donemde butun gonderiler indirimli toplanir ok 6 - esigin altindaki donemde indirim uygulanmaz ok 7 - hacim agirligi yukari yuvarlanir not ok 8 - esikteki gonderi sayisi indirim uretmez expected: 1 actual: 0.88 # tests 8 # pass 7 # fail 1
Aynı düzenleme, aynı kod, farklı sonuç: sekizinci test kırmızı ve beklenen ile gerçekleşen değerler koşumdan okunuyor. Eşik testi, üç yüz altmış dönemlik taramanın bulduğu sapmayı tek bir çağrıyla gösteriyor. Düzeltme, kaymış karşılaştırmayı geri çevirmektir.
// ucret.mjs — ayni ayiklama, karsilastirma yonu korunarak import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs'; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(gonderi) { const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3); return Math.max(gonderi.gram, hacim); } export function indirimliMi(adet) { return adet > INDIRIM_ESIGI; } export function indirimOrani(adet) { return indirimliMi(adet) ? INDIRIM_ORANI : 1; } export function gonderiUcreti(gonderi) { const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus; return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); return Math.round(toplam * indirimOrani(gonderiler.length)); }
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' node fark.mjs
# tests 8 # pass 8 # fail 0 denenen donem: 360 sonucu degisen: 0 ilk ornek: yok
Koşum yeşil ve tarama sıfır sapma buluyor. İki ölçü birlikte okunduğunda anlam kazanır: tarama bu düzenlemenin denenen girdilerde davranışı koruduğunu, bozma denemesi ölçüsü de kümenin altı bozma türünün altısını yakaladığını söylüyor. Davranış koruması bir niyet değil, iki sayıyla ifade edilen bir güvencedir.
Özet
- Yeniden düzenleme adımının davranışı koruduğu iddiası, yeşil koşumdan değil, iki sürümün aynı girdilerde aynı sonucu vermesinden okunur.
- Kaymış tek bir karşılaştırma yönü altı testin altısını yeşil bıraktı; tarama 360 dönemin üçünde sapma buldu ve üçü de tam eşikte toplandı.
- Bir test kümesinin yakalama gücü ölçülebilir: koda uygulanan altı bozma denemesinin dördü yakalandı, ikisi kaçtı ve iki kaçış da sınanmamış sınırdan geldi.
- Sınır davranışı, ayıklama ile ortaya çıkan birimler üzerinden iki testle sabitlendi ve yakalama gücü altıda altıya çıktı.
- Güçlendirilmiş kümede aynı düzenleme sekizinci testi kırmızıya çevirdi; beklenen ile gerçekleşen değer koşumdan doğrudan okundu.
- Bozma denemesi ölçüsü yeterlilik belgesi değil, “testlerim yeterli mi” sorusunu sayıya çeviren bir araçtır.
Kurs Kapanışı
Temiz Kod kursu on altı derste kodun iç yapısını üç açıdan ele aldı.
Adlandırma ve Yapı, okunabilirliği ölçülebilir bir büyüklüğe çevirdi: niyeti açıklayan adlar, yorumun yerini alan adlandırma, tek soyutlama düzeyinde duran küçük fonksiyonlar, girinti ve biçim tutarlılığının bilişsel maliyeti, çevrimsel karmaşıklığın dallanma sayısıyla ölçülmesi ve koşul, arama tablosu ile çok biçimlilik arasındaki seçim.
Arayüz ve Veri Kararları, kararı imzaya taşıdı: boş değer geçirmemek, mantıksal bayrak parametrelerini iki fonksiyona bölmek, en dar görünürlüğü seçmek, kodu değişim nedenine — yani aktöre — göre gruplamak ve aynı işi projede tek biçimde yapmak.
Sadelik İlkeleri, kararların zaman içindeki maliyetini saydı: aynı bilginin tek yerde durması ile tesadüfi benzerliğin yanlış soyutlamaya dönüşmesi, tasarımın küçük adımlarla evrilmesi, gelmemiş gereksinim için yazılan genelleştirmenin bedeli, ayıklama–taşıma–yeniden adlandırma teknikleri ve davranış korumasının ölçülmesi.
On altı dersin ortak yanı, her iddianın bir sayıya bağlanmasıydı: dokunulan dosya sayısı, dallanma sayısı, kapsanmayan yol sayısı, ayrışan sınır sayısı, yakalanan bozma sayısı. Ortak yanları olan başka bir şey daha var ve bu ders sonuna kadar adlandırılmadı: bütün kararlar tek bir programlama biçimi içinde verildi. Fonksiyonlar, sabitler, modüller ve nesne sözlükleri hiç tartışılmadan kullanıldı. Kodun nasıl yazıldığı sorgulandı; hangi modelle düşünüldüğü sorgulanmadı.
Ücret kitaplığı bir dizi fonksiyon ve veri tablosu olarak modellendi. Aynı alan, davranışı ve durumu bir arada tutan nesneler olarak da modellenebilirdi; ya da dönüşümlerin zincirlendiği, durumun hiç değişmediği bir hesap olarak. Bu üç modelin hangisinin seçileceği temiz kod sorusu değil, paradigma sorusudur. Sonraki kurs olan Programlama Paradigmaları bunu ele alır: nesneye dayalı ve fonksiyonel yaklaşımların modelleme farkı, kalıtım yerine bileşimin seçildiği durumlar ve aynı problemin birden çok paradigmayla modellenip karşılaştırılması.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.