Ders 17 / 20
Test
Yerleşik test koşucusu, birim testinin sınırı, sunucuyu dinlemeden ayırarak tümleşim testi yazma, başarısız testin çıktısı ve kapsam raporunun okunması.
İçindekiler
Önceki dört ders ölçüm toplayıcının parçalarını kurdu: özetleyici, yapılandırma, hata sınıfları, kütük ve sunucu. Bu parçaların doğru çalıştığı şimdiye kadar elle, çıktıya bakılarak denetlendi.
Elle denetimin iki kusuru vardır: her değişiklikten sonra yinelenmesi gerekir ve kenar durumlar unutulur. Bu ders denetimi yürütülebilir hâle getirir. Test koşucusu ve doğrulama modülü çalışma zamanının içinde bulunur; belirli bir sürümden itibaren kararlı sayılırlar.
Birim Testi
Birim testi, tek bir modülü kendi başına, dış dünyaya dokunmadan sınar. Sınanan birimin bu yalıtımı destekleyecek biçimde yazılmış olması gerekir — HTTP Sunucusu dersinde özetleme mantığının ayrı modüle alınmasının nedeni buydu.
// ozetleyici.mjs export function satirdanKayit(satir) { const kayit = JSON.parse(satir); if (typeof kayit.dugum !== 'string' || typeof kayit.metrik !== 'string') { throw new TypeError('dugum ve metrik dizgi olmali'); } if (!Number.isFinite(kayit.deger)) { throw new TypeError('deger sonlu bir sayi olmali'); } return kayit; } export class Ozetleyici { #kovalar = new Map(); ekle({ dugum, metrik, deger }) { const anahtar = `${dugum}/${metrik}`; const kova = this.#kovalar.get(anahtar) ?? { sayi: 0, toplam: 0, enBuyuk: -Infinity }; kova.sayi += 1; kova.toplam += deger; kova.enBuyuk = Math.max(kova.enBuyuk, deger); this.#kovalar.set(anahtar, kova); } ozet() { return [...this.#kovalar].map(([anahtar, { sayi, toplam, enBuyuk }]) => ({ anahtar, sayi, ortalama: Number((toplam / sayi).toFixed(2)), enBuyuk, })); } }
// ozetleyici.test.mjs import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { Ozetleyici, satirdanKayit } from './ozetleyici.mjs'; describe('satirdanKayit', () => { test('gecerli satiri kayda cevirir', () => { const kayit = satirdanKayit('{"dugum":"kenar-01","metrik":"nem","deger":48}'); assert.deepEqual(kayit, { dugum: 'kenar-01', metrik: 'nem', deger: 48 }); }); test('bozuk JSON icin SyntaxError firlatir', () => { assert.throws(() => satirdanKayit('bozuk'), SyntaxError); }); test('sayisal olmayan deger icin TypeError firlatir', () => { assert.throws( () => satirdanKayit('{"dugum":"a","metrik":"b","deger":"yirmi"}'), { name: 'TypeError' }, ); }); }); describe('Ozetleyici', () => { test('ayni anahtardaki olcumleri birlestirir', () => { const o = new Ozetleyici(); o.ekle({ dugum: 'kenar-01', metrik: 'sicaklik', deger: 21.4 }); o.ekle({ dugum: 'kenar-01', metrik: 'sicaklik', deger: 22.6 }); assert.deepEqual(o.ozet(), [ { anahtar: 'kenar-01/sicaklik', sayi: 2, ortalama: 22, enBuyuk: 22.6 }, ]); }); test('bos ozetleyici bos dizi dondurur', () => { assert.deepEqual(new Ozetleyici().ozet(), []); }); });
node --test ozetleyici.test.mjs
▶ satirdanKayit ✔ gecerli satiri kayda cevirir (1.11775ms) ✔ bozuk JSON icin SyntaxError firlatir (0.155333ms) ✔ sayisal olmayan deger icin TypeError firlatir (0.074167ms) ✔ satirdanKayit (2.002583ms) ▶ Ozetleyici ✔ ayni anahtardaki olcumleri birlestirir (0.125584ms) ✔ bos ozetleyici bos dizi dondurur (0.046625ms) ✔ Ozetleyici (0.240542ms) ℹ tests 5 ℹ suites 2 ℹ pass 5 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 34.86225
Süre değerleri her çalıştırmada değişir; anlamlı olan sayaçlardır.
node:assert/strict alt yolu, karşılaştırmaları katı eşitlikle yapan biçimi verir.
JavaScript Temelleri kursunda gevşek eşitliğin tuzakları anlatılmıştı; testte gevşek
karşılaştırma, geçmemesi gereken bir testin geçmesine yol açar.
assert.throws çağrısının ikinci argümanı beklenen hatayı tanımlar. Bir yapıcı
verilebilir, bir nesne verilerek alanlar sınanabilir. Yalnızca “bir hata fırlatsın”
demek zayıf bir sınamadır: yanlış nedenle fırlayan hata da testi geçirir.
Bağımlılığı Dışarıdan Vermek
Günlükleme dersindeki kütük oluşturucu, saat fonksiyonunu ve çıktı akışını dışarıdan alıyordu. Bu tasarım tam olarak sınanabilirlik içindir:
// kutuk.test.mjs import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Writable } from 'node:stream'; import { kutukOlustur } from './kutuk.mjs'; function yakalayanCikti() { const satirlar = []; const akis = new Writable({ write(parca, kodlama, bitti) { satirlar.push(parca.toString('utf8')); bitti(); }, }); return { akis, satirlar }; } test('esigin altindaki kayit yazilmaz', () => { const { akis, satirlar } = yakalayanCikti(); const kutuk = kutukOlustur({ duzey: 'uyari', hizmet: 'sinama', cikti: akis, saat: () => 'T' }); kutuk.bilgi('gorunmemeli'); kutuk.uyari('gorunmeli'); assert.equal(satirlar.length, 1); assert.deepEqual(JSON.parse(satirlar[0]), { zaman: 'T', duzey: 'uyari', hizmet: 'sinama', ileti: 'gorunmeli', }); }); test('alt kutuk sabit alanlari her satira ekler', () => { const { akis, satirlar } = yakalayanCikti(); const kutuk = kutukOlustur({ hizmet: 'sinama', cikti: akis, saat: () => 'T' }); kutuk.alt({ istekKimligi: 'r-1' }).bilgi('ilk', { durum: 200 }); assert.equal(JSON.parse(satirlar[0]).istekKimligi, 'r-1'); });
Sabit saat ve toplayan akış sayesinde test, gerçek zamana ve gerçek çıktıya dokunmadan tam kaydı doğrular. Modülün içindeki bir değeri sınama sırasında değiştirmeye gerek kalmaz — bağımlılık zaten dışarıdan verilmektedir.
Tümleşim Testi
Tümleşim testi birkaç parçayı birlikte sınar. HTTP hizmeti için sorun şudur:
hizmet.mjs içe aktarıldığı anda dinlemeye başlar, bu yüzden test içinden
denetlenemez.
Çözüm, sunucuyu kurmakla dinlemeye başlamayı ayırmaktır: modül bir fabrika fonksiyonu dışa aktarır, dinleme kararını çağıran verir.
// sunucu-olustur.mjs import { createServer } from 'node:http'; import { Ozetleyici, satirdanKayit } from './ozetleyici.mjs'; export function sunucuOlustur() { const ozetleyici = new Ozetleyici(); return createServer(async (istek, yanit) => { const adres = new URL(istek.url, `http://${istek.headers.host}`); if (istek.method === 'POST' && adres.pathname === '/olcum') { const parcalar = []; for await (const parca of istek) parcalar.push(parca); const govde = Buffer.concat(parcalar).toString('utf8'); try { let sayac = 0; for (const satir of govde.split('\n')) { if (satir.trim() === '') continue; ozetleyici.ekle(satirdanKayit(satir)); sayac += 1; } yanit.writeHead(201, { 'content-type': 'application/json' }); yanit.end(JSON.stringify({ alinan: sayac })); } catch (hata) { yanit.writeHead(400, { 'content-type': 'application/json' }); yanit.end(JSON.stringify({ hata: hata.message })); } return; } if (istek.method === 'GET' && adres.pathname === '/ozet') { yanit.writeHead(200, { 'content-type': 'application/json' }); yanit.end(JSON.stringify(ozetleyici.ozet())); return; } yanit.writeHead(404, { 'content-type': 'application/json' }); yanit.end(JSON.stringify({ hata: 'bulunamadi' })); }); }
// sunucu.test.mjs import { test, before, after, describe } from 'node:test'; import assert from 'node:assert/strict'; import { once } from 'node:events'; import { sunucuOlustur } from './sunucu-olustur.mjs'; describe('olcum hizmeti', () => { let sunucu; let taban; before(async () => { sunucu = sunucuOlustur(); sunucu.listen(0, '127.0.0.1'); // 0: bos bir baglanti noktasini cekirdek secer await once(sunucu, 'listening'); taban = `http://127.0.0.1:${sunucu.address().port}`; }); after(async () => { sunucu.close(); await once(sunucu, 'close'); }); test('olcum gonderimi 201 doner', async () => { const yanit = await fetch(`${taban}/olcum`, { method: 'POST', body: '{"dugum":"kenar-01","metrik":"nem","deger":48}\n', }); assert.equal(yanit.status, 201); assert.deepEqual(await yanit.json(), { alinan: 1 }); }); test('ozet gonderilen olcumu yansitir', async () => { const yanit = await fetch(`${taban}/ozet`); assert.equal(yanit.status, 200); assert.deepEqual(await yanit.json(), [ { anahtar: 'kenar-01/nem', sayi: 1, ortalama: 48, enBuyuk: 48 }, ]); }); test('bilinmeyen yol 404 doner', async () => { const yanit = await fetch(`${taban}/yok`); assert.equal(yanit.status, 404); }); });
node --test sunucu.test.mjs
▶ olcum hizmeti ✔ olcum gonderimi 201 doner (10.196542ms) ✔ ozet gonderilen olcumu yansitir (1.354708ms) ✔ bilinmeyen yol 404 doner (0.68975ms) ✔ olcum hizmeti (16.061875ms) ℹ tests 3 ℹ suites 1 ℹ pass 3 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 73.281666
İki ayrıntı bu testi güvenilir kılar.
Bağlantı noktası olarak sıfır verildi: çekirdek boş bir geçici bağlantı noktası
seçer ve gerçek numara address() çağrısıyla öğrenilir. Sabit bir numara yazmak,
aynı anda çalışan iki testin çakışmasına yol açar.
after kancası sunucuyu kapatıyor ve kapanmayı bekliyor. Kapatılmayan bir sunucu
olay döngüsünü ayakta tutar ve test süreci bitmez.
Test sırasında istek fetch ile atıldı — barındıran ortamın web platformundan
devraldığı arayüzlerden biri. İstemci için ayrı bir bağımlılık gerekmedi.
Uçtan uca test bir adım daha ileri gider: yapıyı gerçek bir süreç olarak başlatır,
gerçek yapılandırmayla çalıştırır ve dışarıdan istek atar. Alt Süreçler dersindeki
spawn bu iş için kullanılır. Uçtan uca testler yavaş ve kırılgandır; sayıları az
tutulur, kritik yollarla sınırlanır.
Başarısız Test ve Kapsam
Bir testin başarısız olduğunda ne yazdığını görmek, çıktıyı okumayı öğrenmenin en kısa yoludur. Aşağıdaki test bilerek eksik bir beklenti taşıyor:
// basarisiz.test.mjs import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Ozetleyici } from './ozetleyici.mjs'; test('ozet alanlari', () => { const o = new Ozetleyici(); o.ekle({ dugum: 'kenar-01', metrik: 'nem', deger: 48 }); assert.deepEqual(o.ozet(), [{ anahtar: 'kenar-01/nem', sayi: 1, ortalama: 48 }]); });
node --test basarisiz.test.mjs; echo "cikis kodu: $?"
✖ ozet alanlari (0.978292ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 33.174375
✖ failing tests:
test at basarisiz.test.mjs:5:1
✖ ozet alanlari (0.978292ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
{
anahtar: 'kenar-01/nem',
+ enBuyuk: 48,
ortalama: 48,
cikis kodu: 1
Fark raporu, gerçek değerde bulunup beklenende olmayan alanı + ile işaretliyor.
Sıfırdan farklı çıkış kodu, testin bir otomasyon adımından çağrıldığında
başarısızlığı bildirmesini sağlar.
Hangi satırların hiç çalışmadığını görmek için kapsam ölçümü açılır:
node --test --experimental-test-coverage ozetleyici.test.mjs
ℹ start of coverage report ℹ --------------------------------------------------------------- ℹ file | line % | branch % | funcs % | uncovered lines ℹ --------------------------------------------------------------- ℹ ozetleyici.mjs | 93.75 | 90.91 | 100.00 | 4-5 ℹ --------------------------------------------------------------- ℹ all files | 93.75 | 90.91 | 100.00 | ℹ --------------------------------------------------------------- ℹ end of coverage report
Rapor, 4 ve 5 numaralı satırların hiç çalışmadığını söylüyor: bunlar dugum alanı
dizgi olmadığında çalışan dal. Kapsam yüzdesi bir hedef değil bir işarettir — hangi
davranışın hiç sınanmadığını gösterir, sınanan davranışların doğru sınandığını
göstermez.
Özet
- Birim testi tek modülü yalıtımda sınar; bu yalıtım, mantığın arayüzden ayrı bir modülde tutulmasını gerektirir.
- Bağımlılıklar (saat, çıktı akışı) dışarıdan verildiğinde test gerçek zamana ve gerçek çıktıya dokunmadan tam kaydı doğrular.
- Sunucu kurmak ile dinlemeye başlamak ayrıldığında hizmet tümleşim testine açılır; test sıfır numaralı bağlantı noktası ister ve sonunda sunucuyu kapatır.
- Katı doğrulama biçimi gevşek eşitliğin tuzaklarını dışarıda bırakır; fırlatma sınamalarında beklenen hata tipi de belirtilir.
- Kapsam raporu hangi davranışın hiç sınanmadığını gösterir; sınananların doğru sınandığını göstermez.
Sonraki Adım
Testler, beklenen davranışın korunduğunu söyler; beklenmeyen bir davranışın nedenini söylemez. Süreç yavaşladığında, belleği büyüdüğünde veya beklenmedik bir yerde takıldığında başka araçlar gerekir. Sonraki ders çalışma zamanının inceleme arayüzünü, işlemci profilini ve öbek anlık görüntüsünü ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.