Ders 08 / 13
Kırılgan Testler
Gerçekleştirime bağlanan testlerin davranış değişmeden yeniden düzenlemede kırılması, aynı yeniden düzenlemede davranışa bağlı testlerin yeşil kalması ve kırılganlığın düşme oranıyla ölçülmesi.
İçindekiler
Önceki derste testler kodun içine bakmadan yazıldı: girdi verildi, dönüş değeri denetlendi. Altıncı derste ise casus ve sahte nesneler kodun içine baktı — hangi çağrının yapıldığını sınadılar. İki yaklaşım aynı davranışı doğrulayabilir, ama aynı değişiklik karşısında aynı davranmaz.
Bir testin kırılgan (flaky) olması, sınadığı davranış değişmediği hâlde düşmesi demektir. Kırılganlığın iki kaynağı vardır: testin belirlenimci olmayan bir şeye bağlanması ve testin gerçekleştirime bağlanması. Dördüncü ve beşinci dersler birincisini gösterdi. Bu ders ikincisini bir yeniden düzenlemeyle ölçer.
Sınanan Birim
Rezervasyon sırası, ödünçteki bir kitap için bekleyen üyeleri tutar. Sözleşmesi dört maddedir: üye sıraya eklenir ve bekleme konumunu öğrenir, kitap sıradaki ilk üyeye ayrılır, aynı üye iki kez sıraya giremez ve her durum değişiminde üyeye bildirim gider. Ayrıca her işlem bir günlüğe yazılır.
// rezervasyon.mjs — surum 1: dizi tabanli rezervasyon sirasi export function rezervasyonKur({ bildirim, gunluk }) { const sira = []; return { sira, ekle(uyeNo) { if (sira.includes(uyeNo)) throw new Error(`uye zaten sirada: ${uyeNo}`); sira.push(uyeNo); gunluk.yaz(`ekle ${uyeNo} konum ${sira.length}`); bildirim.gonder(uyeNo, `sira: ${sira.length}`); return sira.length; }, sonraki() { const uyeNo = sira.shift(); if (uyeNo === undefined) return undefined; gunluk.yaz(`sonraki ${uyeNo}`); bildirim.gonder(uyeNo, 'kitap ayrildi'); return uyeNo; }, uzunluk: () => sira.length, }; }
// casus.mjs — cagri kaydi tutan kucuk sahte bagimlilik export function casusNesne(...yontemler) { const cagrilar = []; const nesne = { cagrilar }; for (const ad of yontemler) nesne[ad] = (...girdi) => cagrilar.push([ad, ...girdi]); return nesne; }
İki Test Kümesi
Birinci küme yalnız dışarıdan gözlenebilen şeyleri sınar: dönüş değerleri, fırlatılan hata ve üyeye giden bildirim. Üyeye giden bildirim bir yan etkidir ve casus nesneyle doğrulanır — ama sözleşmenin parçası olduğu için davranış sayılır.
// davranis.test.mjs — yalniz disaridan gozlenen davranisi sinar import { test } from 'node:test'; import assert from 'node:assert/strict'; import { rezervasyonKur } from './rezervasyon.mjs'; import { casusNesne } from './casus.mjs'; const kur = () => { const bildirim = casusNesne('gonder'); return { kuyruk: rezervasyonKur({ bildirim, gunluk: casusNesne('yaz') }), bildirim }; }; test('ekleme sirasi bekleme konumunu belirler', () => { const { kuyruk } = kur(); assert.equal(kuyruk.ekle('U-17'), 1); assert.equal(kuyruk.ekle('U-42'), 2); }); test('kitap siradaki ilk uyeye ayrilir', () => { const { kuyruk } = kur(); kuyruk.ekle('U-17'); kuyruk.ekle('U-42'); assert.equal(kuyruk.sonraki(), 'U-17'); assert.equal(kuyruk.sonraki(), 'U-42'); assert.equal(kuyruk.sonraki(), undefined); }); test('ayni uye siraya iki kez giremez', () => { const { kuyruk } = kur(); kuyruk.ekle('U-17'); assert.throws(() => kuyruk.ekle('U-17'), /uye zaten sirada: U-17/); assert.equal(kuyruk.uzunluk(), 1); }); test('uyeye konumunu bildiren ileti gider', () => { const { kuyruk, bildirim } = kur(); kuyruk.ekle('U-17'); kuyruk.sonraki(); assert.deepEqual(bildirim.cagrilar, [ ['gonder', 'U-17', 'sira: 1'], ['gonder', 'U-17', 'kitap ayrildi'], ]); });
İkinci küme aynı birimi sınar, ama iç yapıya ve günlük metnine bağlanır. İkisi de sözleşmenin dışındadır: sırayı diziyle mi eşlemeyle mi tuttuğu, günlüğe hangi cümleyi yazdığı üyeyi ilgilendirmez.
// gerceklestirim.test.mjs — ic yapiya ve gunluk metnine baglanan testler import { test } from 'node:test'; import assert from 'node:assert/strict'; import { rezervasyonKur } from './rezervasyon.mjs'; import { casusNesne } from './casus.mjs'; const kur = () => { const gunluk = casusNesne('yaz'); return { kuyruk: rezervasyonKur({ bildirim: casusNesne('gonder'), gunluk }), gunluk }; }; test('sira ic dizisi ekleme sirasini tutar', () => { const { kuyruk } = kur(); kuyruk.ekle('U-17'); kuyruk.ekle('U-42'); assert.deepEqual(kuyruk.sira, ['U-17', 'U-42']); }); test('gunluge yazilan satirlar belirli bicimdedir', () => { const { kuyruk, gunluk } = kur(); kuyruk.ekle('U-17'); kuyruk.sonraki(); assert.deepEqual(gunluk.cagrilar, [ ['yaz', 'ekle U-17 konum 1'], ['yaz', 'sonraki U-17'], ]); });
node --test --test-reporter=tap davranis.test.mjs gerceklestirim.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - ekleme sirasi bekleme konumunu belirler ok 2 - kitap siradaki ilk uyeye ayrilir ok 3 - ayni uye siraya iki kez giremez ok 4 - uyeye konumunu bildiren ileti gider ok 5 - sira ic dizisi ekleme sirasini tutar ok 6 - gunluge yazilan satirlar belirli bicimdedir # tests 6 # pass 6 # fail 0
Altı testin altısı da yeşil. Bu noktada iki kümenin farkı görünmez; ikisi de aynı kodu doğruluyor gibidir.
Yeniden Düzenleme
Sıra bir eşlemeye taşınır, iç dizi dışarıya açılmaz ve günlük satırları tek bir kalıba oturtulur. Sözleşmenin dört maddesi de aynen korunur: dönüş değerleri, fırlatılan hata ve üyeye giden iletiler değişmez.
// rezervasyon.mjs — surum 2: sira eslemeye tasindi, gunluk bicimi tek kaliba baglandi export function rezervasyonKur({ bildirim, gunluk }) { const kayitlar = new Map(); let sonSiraNo = 0; const siradakiler = () => [...kayitlar.entries()] .sort((a, b) => a[1] - b[1]) .map(([uyeNo]) => uyeNo); return { ekle(uyeNo) { if (kayitlar.has(uyeNo)) throw new Error(`uye zaten sirada: ${uyeNo}`); sonSiraNo += 1; kayitlar.set(uyeNo, sonSiraNo); const konum = kayitlar.size; gunluk.yaz(`rezervasyon uye=${uyeNo} durum=eklendi konum=${konum}`); bildirim.gonder(uyeNo, `sira: ${konum}`); return konum; }, sonraki() { const [ilk] = siradakiler(); if (ilk === undefined) return undefined; kayitlar.delete(ilk); gunluk.yaz(`rezervasyon uye=${ilk} durum=ayrildi`); bildirim.gonder(ilk, 'kitap ayrildi'); return ilk; }, uzunluk: () => kayitlar.size, }; }
Aynı iki dosya, aynı komut, yeniden düzenlenmiş kitaplık.
node --test --test-reporter=tap davranis.test.mjs gerceklestirim.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - ekleme sirasi bekleme konumunu belirler ok 2 - kitap siradaki ilk uyeye ayrilir ok 3 - ayni uye siraya iki kez giremez ok 4 - uyeye konumunu bildiren ileti gider not ok 5 - sira ic dizisi ekleme sirasini tutar not ok 6 - gunluge yazilan satirlar belirli bicimdedir # tests 6 # pass 4 # fail 2
Ölçüm nettir: dört davranış testi yeşil kaldı, iki gerçekleştirim testi düştü. Kitaplığın sözleşmesi değişmedi — hiçbir üye farklı bir sonuç görmez. Düşen iki test bir gerileme bildirmedi; kendi varsayımlarının bozulduğunu bildirdi.
Bu, kırılganlığın maliyetidir. Yeniden düzenleme, davranışı korurken iç yapıyı iyileştirmek demektir; bunu her yaptığında düşen bir test kümesi, yeniden düzenlemeyi pahalı kılar. Yeterince pahalı olduğunda yapılmaz olur ve kod, kendisini koruyan testler yüzünden iyileştirilemez hâle gelir.
Buradan çıkan sınır sorusu şudur: bir iddia, sözleşmenin parçası olan bir şeye mi bağlı? Dördüncü davranış testi de bir casus nesne kullanıyor ve o da bir yan etkiyi sınıyor — ama üyeye giden bildirim sözleşmededir. Ayrımı yapan şey aracın türü değil, doğrulanan şeyin kimin gözlediği bir şey olduğudur.
Kırılganlığın Ölçüsü
Kırılganlığın diğer kaynağı belirlenimci olmayan girdilerdir ve orada ölçü düşme oranıdır (flake rate): aynı testin aynı kodda yapılan çok sayıda koşumunda düşme yüzdesi.
// dusme-orani.mjs — kirilgan bir testin dusme orani sabit tohumla olculur function uretec(tohum) { let durum = (tohum * 2654435761) % 2147483647; return () => { durum = (durum * 48271) % 2147483647; return durum / 2147483647; }; } // Toplu isin suresi makineye ve yuke gore degisir: kirk ile altmis dokuz arasi. const sureUret = (rastgele) => 40 + Math.floor(rastgele() * 30); function butceyeBagliTest(rastgele) { const sure = sureUret(rastgele); if (sure > 68) throw new Error(`sure butcesi asildi: ${sure}`); } function davranisaBagliTest(rastgele) { const sure = sureUret(rastgele); if (Number.isInteger(sure) === false || sure < 0) throw new Error(`gecersiz sure: ${sure}`); } const KOSUM = 1000; for (const [ad, test] of [['sure butcesine bagli', butceyeBagliTest], ['davranisa bagli', davranisaBagliTest]]) { const rastgele = uretec(20); let dusen = 0; for (let i = 0; i < KOSUM; i += 1) { try { test(rastgele); } catch { dusen += 1; } } const oran = ((dusen / KOSUM) * 100).toFixed(1); console.log(`${ad.padEnd(21)} ${KOSUM} kosumda ${dusen} dusus, dusme orani %${oran}`); }
sure butcesine bagli 1000 kosumda 27 dusus, dusme orani %2.7 davranisa bagli 1000 kosumda 0 dusus, dusme orani %0.0
Yüzde iki virgül yedilik bir oran, tam da fark edilmesi güç olduğu için tehlikelidir. Elli koşumun kırk dokuzunda yeşil olan bir test “arada bir düşüyor” diye anılır ve düştüğü koşum yeniden çalıştırılır. O anda takımın anlamı bozulur: yeşil bir koşum artık “kod doğru” demez, “bu sefer şanslıydık” demeye başlar.
Düşme oranı ölçüldüğünde yapılacak iş bellidir. Kırılgan test karantinaya alınır — ana koşumdan çıkarılır ki kalan testlerin sinyali temiz kalsın — ve kaynağı düzeltilir. Yukarıdaki örnekte düzeltme, süre bütçesini bir birim testine yükletmemektir; süre bir başarım ölçütüdür ve ayrı ölçülür.
Özet
- Kırılgan test, sınadığı davranış değişmediği hâlde düşen testtir; iki kaynağı belirlenimci olmayan girdiler ve gerçekleştirime bağlanmadır.
- Sıra dizisi eşlemeye taşındığında ve günlük biçimi değiştiğinde dört davranış testi yeşil kaldı, iki gerçekleştirim testi düştü; kitaplığın sözleşmesi hiç değişmemişti.
- Gerçekleştirime bağlı testler yeniden düzenlemeyi pahalı kılar; yeterince pahalı olduğunda kod, kendisini koruyan testler yüzünden iyileştirilemez hâle gelir.
- Sınırı çizen soru araç değil kapsamdır: doğrulanan şey sözleşmenin parçası mı, yoksa iç ayrıntı mı?
- Belirlenimci olmayan kırılganlığın ölçüsü düşme oranıdır; ölçümde süre bütçesine bağlanan test bin koşumun yirmi yedisinde düştü, davranışa bağlanan test hiç düşmedi.
Sonraki Adım
Birim Test Pratiği konusu burada kapanıyor. Buraya kadar her testte kod önce vardı: bir hata raporu, bir kural ya da bir yeniden düzenleme, sınanacak bir gerçekleştirimi hazır buldu. Sonraki konu bu sırayı tersine çevirir. Test önce yazıldığında hangi kodun yazılacağını test belirler; kırmızı bir koşumla başlayan döngü, yeşile ulaştıktan sonra yeniden düzenlemeyle biter. Sonraki ders bu üç adımı tek tek koşturur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.