Ders 04 / 13
Hızlı ve Bağımsız Testler
Modül düzeyinde paylaşılan durumun ürettiği sıra bağımlılığının sabit tohumlu permütasyonla gösterilmesi, taze düzenekle kırılması ve süreç dışı düzeneğin koşum süresine eklediği payın ölçülmesi.
İçindekiler
Önceki dersteki testlerin hepsi tek başına doğruydu ve hepsi tek başına kaldığı sürece doğru kalır. Testler çoğaldıkça bu koşul kendiliğinden sürmez. İki test aynı nesneyi, aynı modül değişkenini ya da aynı dosyayı kullanıyorsa, birincinin bıraktığı durum ikincinin girdisine karışır. Sonuç, koşum sırasının bir girdiye dönüşmesidir.
Sıra bağımlılığı sinsi bir kusurdur, çünkü çoğu zaman görünmez: koşucu testleri hep aynı sırada çalıştırdığı sürece takım yeşil kalır. Bağımlılık, dosyaya yeni bir test eklendiğinde ya da testler paralel dağıtıldığında ortaya çıkar ve o an bozulanın kod mu test mi olduğu belli değildir. Bu dersin ilk işi, o sırayı bir değişkene çevirip bağımlılığı görünür kılmaktır.
Sırayı Bir Değişkene Çevirmek
Sıra bağımlılığını aramanın yolu, koşum sırasını denetimli biçimde değiştirmektir. Bunun için rastgeleliğe ihtiyaç yoktur; sabit tohumdan (seed) türeyen bir permütasyon yeterlidir ve tekrarlanabilir olduğu için daha kullanışlıdır. Frontend Kalitesi kursundaki Test Kararlılığı dersinde tanıtılan doğrusal eşlenik üreteç bu iş için yeterli bir kaynaktır.
// kosucu.mjs — sabit tohumlu sira karistirmali kucuk kosucu export function uretec(tohum) { let durum = (tohum * 2654435761) % 2147483647; return () => { durum = (durum * 48271) % 2147483647; return durum / 2147483647; }; } export function karistir(dizi, tohum) { const rastgele = uretec(tohum); const kopya = [...dizi]; for (let i = kopya.length - 1; i > 0; i -= 1) { const j = Math.floor(rastgele() * (i + 1)); [kopya[i], kopya[j]] = [kopya[j], kopya[i]]; } return kopya; } export function kos(testler, tohum) { const sira = karistir(Object.keys(testler), tohum); let gecen = 0; const satirlar = []; for (const ad of sira) { try { testler[ad](); gecen += 1; satirlar.push(` ok ${ad}`); } catch (hata) { satirlar.push(` not ok ${ad} (${hata.message.split('\n')[0]})`); } } console.log(`tohum ${tohum}: ${gecen} gecti, ${sira.length - gecen} dustu`); console.log(satirlar.join('\n')); }
Aynı tohum her zaman aynı sırayı verir; düşen bir koşum bildirildiğinde tohumu paylaşmak sorunu yeniden üretmeye yeter.
Paylaşılan Durumun Ürettiği Bağımlılık
Aşağıdaki katalog modülü, ödünç kayıtlarını modül düzeyinde bir eşlemede tutar. Modül bir kez yüklenir ve süreç boyunca tek bir eşleme vardır.
// katalog.mjs — modul duzeyinde paylasilan odunc kaydi const kayitlar = new Map(); export function oduncEkle(uyeNo, kitapNo) { if (kayitlar.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`); kayitlar.set(kitapNo, uyeNo); } export function iadeEt(kitapNo) { kayitlar.delete(kitapNo); } export function acikOduncSayisi() { return kayitlar.size; }
Üç test bu modülün üç ayrı kuralını sınar. Her biri tek başına okunduğunda doğrudur.
// paylasilan.mjs — paylasilan durum uzerinde tohumla belirlenen sirayla kosum import assert from 'node:assert/strict'; import { kos } from './kosucu.mjs'; import { oduncEkle, iadeEt, acikOduncSayisi } from './katalog.mjs'; const TESTLER = { 'yeni odunc katalogda gorunur': () => { oduncEkle('U-17', 'K-903'); assert.equal(acikOduncSayisi(), 1); }, 'iade edilen kitap katalogdan duser': () => { oduncEkle('U-17', 'K-101'); iadeEt('K-101'); assert.equal(acikOduncSayisi(), 0); }, 'ayni kitap iki kez odunc verilemez': () => { oduncEkle('U-42', 'K-903'); assert.throws(() => oduncEkle('U-58', 'K-903'), /zaten odunc verilmis/); }, }; kos(TESTLER, Number(process.argv[2] ?? 3));
İki farklı tohumla iki ayrı süreçte koşturmak yeter. Ayrı süreç gerekir, çünkü modül düzeyindeki eşleme süreç ömrü boyunca yaşar.
node paylasilan.mjs 3 node paylasilan.mjs 4
tohum 3: 1 gecti, 2 dustu ok yeni odunc katalogda gorunur not ok iade edilen kitap katalogdan duser (Expected values to be strictly equal:) not ok ayni kitap iki kez odunc verilemez (kitap zaten odunc verilmis: K-903) tohum 4: 2 gecti, 1 dustu ok iade edilen kitap katalogdan duser ok yeni odunc katalogda gorunur not ok ayni kitap iki kez odunc verilemez (kitap zaten odunc verilmis: K-903)
Aynı kod, aynı üç test, iki farklı sonuç. Üçüncü tohumda geçen bir test dördüncüde düşer. Bu tabloda hangi sonucun “doğru” olduğu sorusunun yanıtı yoktur, çünkü testler bir kuralı değil, kendilerinden önce ne çalıştığını ölçüyor.
İki ayrı kirlenme biçimi görünüyor. iade edilen kitap katalogdan duser testi mutlak bir
sayı bekliyor: kataloğun boş olduğunu varsayıyor. ayni kitap iki kez odunc verilemez
testi ise tersine, kataloğun kendi eklediği kitabı içermediğini varsayıyor. İkisi de
başlangıç durumu hakkında yazılı olmayan bir varsayım taşıyor ve o varsayımı önceki testler
belirliyor.
Taze Düzenek
Çözüm, testleri sıraya duyarsız kılmak değil, her testin kendi başlangıç durumunu kurmasıdır. Bunun için paylaşılan tek eşleme yerine, her çağrıda taze bir katalog üreten bir fabrika kullanılır.
// katalog.mjs — surum 2: her cagrida taze bir katalog uretilir export function katalogKur() { const kayitlar = new Map(); return { oduncEkle(uyeNo, kitapNo) { if (kayitlar.has(kitapNo)) throw new Error(`kitap zaten odunc verilmis: ${kitapNo}`); kayitlar.set(kitapNo, uyeNo); }, iadeEt(kitapNo) { kayitlar.delete(kitapNo); }, acikOduncSayisi() { return kayitlar.size; }, }; }
Bir testin çalışmaya başlamadan önce kurduğu bu başlangıç durumuna düzenek (fixture) denir. Test koşucusu düzeneği kurmak ve gerekiyorsa sökmek için kancalar sağlar; her testten önce çalışan kanca kuruluma, her testten sonra çalışan kanca yıkıma karşılık gelir.
// bagimsiz.test.mjs — her teste taze duzenek, bildirim sirasi tohumdan gelir import { test, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { karistir } from './kosucu.mjs'; import { katalogKur } from './katalog.mjs'; let katalog; beforeEach(() => { katalog = katalogKur(); }); const TESTLER = { 'yeni odunc katalogda gorunur': () => { katalog.oduncEkle('U-17', 'K-903'); assert.equal(katalog.acikOduncSayisi(), 1); }, 'iade edilen kitap katalogdan duser': () => { katalog.oduncEkle('U-17', 'K-101'); katalog.iadeEt('K-101'); assert.equal(katalog.acikOduncSayisi(), 0); }, 'ayni kitap iki kez odunc verilemez': () => { katalog.oduncEkle('U-42', 'K-903'); assert.throws(() => katalog.oduncEkle('U-58', 'K-903'), /zaten odunc verilmis/); }, }; for (const ad of karistir(Object.keys(TESTLER), Number(process.env.TOHUM ?? 3))) { test(ad, TESTLER[ad]); }
Testler artık yerleşik koşucuyla çalışır; bildirim sırası tohumdan gelir, düzenek her testten önce yeniden kurulur.
TOHUM=3 node --test --test-reporter=tap bagimsiz.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))' TOHUM=4 node --test --test-reporter=tap bagimsiz.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - yeni odunc katalogda gorunur ok 2 - iade edilen kitap katalogdan duser ok 3 - ayni kitap iki kez odunc verilemez # tests 3 # pass 3 # fail 0 ok 1 - iade edilen kitap katalogdan duser ok 2 - yeni odunc katalogda gorunur ok 3 - ayni kitap iki kez odunc verilemez # tests 3 # pass 3 # fail 0
Sıra değişti, sonuç değişmedi. Bağımsızlığın ölçütü budur: koşum sırası bir girdi olmaktan çıktığında testin bildirdiği şey yalnızca sınadığı kuraldır.
Aynı ilkenin iki uygulama sonucu vardır. Testler arasında paylaşılan hiçbir yazılabilir durum kalmadığı için testler paralel koşturulabilir. Düşen bir test tek başına koşturulduğunda da düşer; yeniden üretmek için bütün takımı çalıştırmak gerekmez.
Düzeneğin Süre Payı
Bağımsızlık ile hız ayrı erdemler gibi görünür, ama aynı kaynaktan beslenir. Her testin kendi düzeneğini kurması, düzeneğin ucuz olmasını gerektirir. Bellekteki bir eşlemeyi kurmak birkaç mikrosaniyedir; aynı düzeneği diskte kurmak süreç dışına çıkmayı gerektirir.
// hiz.mjs — bellek ici duzenek ile disk destekli duzenegin karsilastirmasi import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const TEST_SAYISI = 200; function bellekIciDuzenek() { let dosyaIslemi = 0; for (let i = 0; i < TEST_SAYISI; i += 1) { const kayitlar = new Map([['K-903', 'U-17']]); if (kayitlar.size !== 1) throw new Error('duzenek bozuk'); } return dosyaIslemi; } function diskDestekliDuzenek() { let dosyaIslemi = 0; const kok = mkdtempSync(join(tmpdir(), 'katalog-')); for (let i = 0; i < TEST_SAYISI; i += 1) { const yol = join(kok, `katalog-${i}.json`); writeFileSync(yol, JSON.stringify({ 'K-903': 'U-17' })); dosyaIslemi += 1; const kayitlar = new Map(Object.entries(JSON.parse(readFileSync(yol, 'utf8')))); dosyaIslemi += 1; if (kayitlar.size !== 1) throw new Error('duzenek bozuk'); } rmSync(kok, { recursive: true, force: true }); return dosyaIslemi; } function olc(islev) { const baslangic = performance.now(); const dosyaIslemi = islev(); return { sure: performance.now() - baslangic, dosyaIslemi }; } const bellek = olc(bellekIciDuzenek); const disk = olc(diskDestekliDuzenek); console.log(`bellek ici duzenek : ${TEST_SAYISI} test, ${bellek.dosyaIslemi} dosya islemi`); console.log(`disk destekli duzenek: ${TEST_SAYISI} test, ${disk.dosyaIslemi} dosya islemi`); console.log(`disk destekli surum daha uzun surdu : ${disk.sure > bellek.sure}`); console.log(`sure farki en az yirmi kat : ${disk.sure / bellek.sure >= 20}`);
bellek ici duzenek : 200 test, 0 dosya islemi disk destekli duzenek: 200 test, 400 dosya islemi disk destekli surum daha uzun surdu : true sure farki en az yirmi kat : true
Mutlak süreler makineye ve dosya sistemine bağlıdır; bu yüzden çıktı ham milisaniye yerine karşılaştırma sonucu bildiriyor. Değişmeyen şey ilişkinin yönüdür: iki yüz testlik bir takım, test başına iki dosya işlemiyle dört yüz süreç dışı çağrı üretir ve bu çağrıların her biri sabit bir taban maliyet taşır.
Buradan çıkan kural bir hız hedefi değil, bir sınır tanımıdır. Birinci derste “birim” sınırının süreç dışı kaynakları dışarıda bıraktığı söylenmişti; bu ölçüm o kısıtın bedelini gösteriyor. Diskle, ağla ya da veri tabanıyla çalışan testler yazılmayacak demek değildir — o testlerin adı tümleşim testidir ve ayrı bir katmanda, daha seyrek koşturulur.
Özet
- Paylaşılan yazılabilir durum, koşum sırasını testin girdilerinden birine dönüştürür.
- Sabit tohumdan türeyen bir permütasyon sıra bağımlılığını tekrarlanabilir biçimde ortaya çıkarır; aynı üç test iki tohumda iki farklı sonuç verdi.
- Kirlenme iki yönde işler: bir test kataloğun boş, başka bir test kataloğun dolu olmadığını varsaydı; ikisinin de varsayımını önceki testler belirledi.
- Her testin kendi düzeneğini kurması sırayı sonuçtan bağımsız kılar ve testleri paralel koşturulabilir yapar.
- Bağımsızlık ucuz düzenek gerektirir: iki yüz testlik bir takımda süreç dışı düzenek dört yüz dosya işlemi ekledi ve koşumu belirgin biçimde uzattı.
Sonraki Adım
Bu derste düzenek testin elindeydi: katalog nesnesi bir fabrika çağrısıyla üretiliyordu. Sınanan kod bağımlılığını kendi içinde yarattığında bu olanak kalmaz — testin değiştiremeyeceği bir saat, bir rastgelelik kaynağı ya da bir depo, testin girdisi olmaktan çıkar. Sonraki ders aynı iş kuralını iki biçimde yazar, bağımlılığın içeriden yaratıldığı sürümle dışarıdan verildiği sürümü karşılaştırır ve testin kurduğu düzeneği satır sayısıyla ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.