Test Test Diye Nice Nice: Mocklama (3) - Timer
Bölümler
- Test Test Diye Nice Nice: Mocklama (1) - Fonksiyon
- Test Test Diye Nice Nice: Mocklama (2) - Modül
- Test Test Diye Nice Nice: Mocklama (3) - Timer (Şu anda buradasınız)
- Test Test Diye Nice Nice: Mocklama (4) - React
İçerik
Evren yaratıldığı anda gizli bir zamanlayıcı saymaya başladı. Bu uçsuz bucaksız düzlemde her birimize bir şerit ayrıldı ve yaşadığımız tüm olaylar bu şeride yazılmaya başlandı. Bazen anılarımız kesişti ve şeritler birleşti, bazense ummadığımız kadar uzaklaştılar birbirlerinden... Bu ne alaka diyebilirsiniz aniden zihnimde belirince dümenden paylaştım. Garip blog dünyama hoşgeldiniz.
Günlük hayatımızda bir takım işlerimizi zamanlayıcılara göre yaparız. 40 dk
sonra yemek yiyecek ya da 24 saatte bir ilaç içecek olabiliriz. Kodlarımızdaki
eylemleri yapmak için de setTimeout()
ve setInterval()
gibi zamanlayıcı
fonksiyonlardan faydalanırız. Bu makalede zamanlayıcı fonksiyonların mocklanış
biçimlerine değineceğiz.
Makaleye güzel bir arkaplan müziğinin eşlik etmesini isteyenlere fazlaca dinlediğim keman virtüözü Farid Farjad'ı takdim ediyorum:
Yazının kodlarına Github üzerinden erişebilirsiniz.
Girizgah
Her zamanki gibi konunun temeline inmek için bir örneği baz alacağız. Parametre olarak aldığı fonksiyonu 3 saniye sonra çalıştıran bol konsollu bir fonksiyonumuz olduğunu varsayalım.
=== Output:setTimeout çalışmadan önce setTimeout çalıştıktan sonra callback çalışmadan önce callback çalıştı callback çalıştıktan sonra
Fonksiyon çalıştırılır, callback fonksiyonu event loop'a eklenir ve 3 saniye
tamamlanınca çalıştırılır. Bu akışı aklımızın bir köşesinde tutarak elimizdeki
bilgilerle test yazalım. Hatırlayacağımız üzere jest.spyOn
ile objeler
üzerinden metodları mockluyorduk. Built-in metodlar için global
objesini
kullanabiliriz.
Testlerimizi bozmaması için 60-62. satırları silmeyi unutmayın. Aksi halde testte bir kez çağırılmasını bekleyip iki kez çağırıldığını görürsünüz. Event loop hakkındaki bilginizi tazelemek isterseniz de şu videoya göz atabilirsiniz: Olay döngüsü nedir? | Philip Roberts | JSConf AB
=== Output:console.log
setTimeout çalışmadan önce
setTimeout çalıştıktan sonra
FAIL src/playground/index.test.ts
✕ should call callback after 3 second
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
İlk expect
başarıyla geçer ancak ikincisinde fonksiyonun tamamlanması
beklenmediği için test başarısız olur. Fonksiyonun 3 saniye sonra
tamamlanacağını belirtmemiz gerekir.
=== Output:PASS src/playground/index.test.ts ✓ should call callback after 3 second
Anlaşılacağı üzere async fonksiyonların temelinde basit bir matematik var:
Kod bekliyorsa test de beklesin. Callback 3 saniye sonra çalışacağından ötürü
expect'i de 3 saniye sonra çalıştırırsak çağrıldığından emin olabiliriz. Testin
callback'ine iletilen done
metodu çağrılmadığı takdirde testin sona erdiğini
anlayamaz.
Ancaaak, burada bir problem var. Bekleyeceğimiz süre 3 saniye değil de 24 saat falan olsaydı ne olacaktı?
Bununla ancak günü kurtarırız. Gerçek bir çözüme geçelim.
Fake Timer
JS'de zamanlayıcı metodların baz aldığı bir timer bulunur. Testlerde bunu mocklamak için jest.useFakeTimers(fakeTimersConfig?)
metodu kullanılır. Nereden çağrıldığı farketmeksizin tüm dosyayı etkiler ve her
çağrıldığında önceki zamanlayıcılara ait bilgileri temizler. Kodun bir kısmında
gerçek zamanlayıcıya geri dönmek istersek jest.useRealTimers()
kullanabiliriz.
=== Output:
FAKES
-------
setTimeout(): function () {
return clock[method].apply(clock, arguments);
}
setInterval(): function () {
return clock[method].apply(clock, arguments);
}
Date.now(): function now() {
return target.clock.now;
}
REALS
-------
setTimeout(): function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: ``window, repeat: false });
}
setInterval(): function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: ``window, repeat: true });
}
Date.now(): function now() { [native code] }
Sahte zamanlayıcıyı özelleştirmek istersek aşağıdaki değerleri alabilen bir obje sağlayabiliriz:
- advanceTimers (boolean | number) - gerçek zamanlayıcıya göre ne kadar
hızlı ilerleyeceğini belirtebiliriz.
true
ayarlanırsa gerçekte 1ms ilerleniyorsa fake timer'da 1ms ilerler. Bir sayı iletirsek gerçekteki 1ms'e karşılık o sayı kadar daha hızlı ilerler. - doNotFake (Array) - fake timer kullanmak istemediğimiz metodların listesi.
- now (number | Date) - fake timerların kullanacağı sistem tarihi.
Varsayılan olarak
Date.now()
değerini yani günümüz tarihini alır. - timerLimit - az sonra değineceğimiz
jest.runAllTimers()
ile çalıştırabileceğimiz maksimum timer sayısı.
=== Output:
setTimeout(): function () {
return clock[method].apply(clock, arguments);
}
setInterval(): function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: window, repeat: true });
}
Date.now(): 1999-03-20T22:00:00.000Z
Callback'i Beklemeksizin Çalıştırmak
Zamanlayıcı metodlarının callback'leri event loop'da kuyruğa alınır ve süresi
tamamlandığında kuyruktan çıkarılır ve çalıştırılır. Testlerde timeout'u es
geçip callback'i anında çalıştırabiliriz. setTimeout()
ile yazdığımız
testi tekrardan görelim.
Süre az gözükse bile 3 saniye 3 saniye birike birike pipeline olur size 1.5 saat. Sonra deployment yolu gözlersiniz. Dolayısıyla hiç bu işlere girmeden callback'leri sıralarını baz alarak timeout'suz çalıştırmak için jest.runAllTimers()
metodunu kullanabiliriz.
Test esnasında tamamlanmayı bekleyen timer sayısını görmek için
jest.getTimerCount()
metodunu kullanabiliriz.
=== Output:kalan fake timer sayısı: 0 setTimeout çalışmadan önce setTimeout çalıştıktan sonra kalan fake timer sayısı: 1 callback çalışmadan önce callback çalıştıktan sonra kalan fake timer sayısı: 0
PASS src/playground/index.test.ts ✓ should wait 10 second before call callback (29 ms)
Test başarıyla geçecektir. Zamanlayıcıların timeout'larını sıfırlamak için iki ana metodumuz bulunur:
jest.runAllTicks()
- micro-task (process.nextTick) kuyruğunda bulunan ve bunlardan türeyen callback'leri çalıştırır.jest.runAllTimers()
- macro-task (setTimeout, setInterval, setImmediate) kuyruğunda bulunan ve bunlardan türeyen callback'leri çalıştırır.
Micro-macro görevler konusunda kafanızda soru işareti varsa sizi yine şu videoya alalım.
Yalnızca Halihazırda Kuyrukta Bulunan Callback'leri Çalıştırma
Önceki başlıkta bahsettiğimiz üzere jest.runAllTimers()
metodu, fonksiyon
kuyruktaki macro-task'ların callback'lerini çalıştırır. Türetilenler varsa
onları da sırasıyla kuyruğa ekleyerek kuyruk boşalana kadar devam eder.
=== Output:Called callback 1. Called callback 2. Called child callback 2. Called childest callback 1. Called child callback 1.
Örnekteki event loop akışını görselleştirmek isterseniz JavaScript Visualizer 9000 sitesine eklediğim örneğe göz atabilirsiniz.
jest.runOnlyPendingTimers()
metoduysa yalnızca o anda macro-task kuyruğunda
bulunan callback'leri çalıştırır. Callback'lerden türetilmiş macro-task'lara
dokunmaz.
=== Output:Called callback 1. Called callback 2.
Zamanlayıcıyı Belirli Süre İlerleterek Callback'leri Çalıştırma
Tüm zamanlayıcıların süresini sıfırlamak istemiyorsak ikinci seçenek olarak zamanlayıcıyı belirli süre ilerletebiliriz. Örnekteki sahte zamanlayıcıyı 4000ms ilerleterek çıktıyı analiz edelim.
=== Output:Called callback 1. Called callback 2. Called child callback 2. Called child callback 3.
Kümülatif olarak toplandığında verilen süre içerisinde çalıştırılan timerların callback'leri timeout beklemeksizin çalıştırılır.
Callback'leri Adım Adım Çalıştırma
Son yöntem ise callback'lerin adım adım çalıştırılmasını sağlayan
jest.advanceTimersToNextTimer(step?)
metodudur.
=== Output:Step 1:
Called callback 1.
Called callback 2.
Step 2:Called child callback 2.
Called child callback 3.
Step 3:Called childest callback 1.
Görebileceğimiz gibi önce bir setTimeout
'un callback'ini ardından içindekini
ve ardından onun içindeki çalıştırarak ilerleyebiliriz. Teker teker ilerlemek
yerine birden fazla adımı tek seferde atmak istersek step
parametresini
kullanabiliriz.
=== Output:Step 1-2:
Called callback 1.
Called callback 2.
Called child callback 2.
Called child callback 3.
Ekstra Metodlar
Son olarak üç minik metoddan bahsedelim:
jest.now()
- o anki tarihi ms olarak döndürür. Kullanılıyor ise fake zamanlayıcı değerini, aksi halde gerçek zamanlayıcı değerini verir.jest.setSystemTime(now?: number | Date)
- test içerisinde sistem tarihini değiştirir.jest.getRealSystemTime()
- günümüz tarihini verir.
=== Output:gerçek tarih (orjinal timer'a göre): 2022-11-15T03:32:20.747Z sahte tarih (fake timer'a göre): 1999-03-20T22:00:00.000Z
Pratik
Sona gelirken testini yazmanızı beklediğim bir fonksiyon bırakıyorum. Çıktıdan case'leri çıkararak kendi kendinize yazmanızı tavsiye ederim çünkü çok fazle şey öğrenilen anlar genellikle en çok zorlanılan zamanlar oluyor. Takılırsanız Github'dan çözümüne göz atabilirsiniz.
=== Output:
PASS src/playground/utils.test.ts
breakReminder() tests
✓ should start work on first iteration (3 ms)
✓ should call breakActivity on second, third and fourth iteration (1 ms)
✓ should end work on last iteration (6 ms)
Kapanış
Kısa ve öz bir bölüm oldu. Timer'ların nasıl tepki verdiğini özümsemek için kod üzerinde bol bol denemenizi tavsiye ederim.
Yazı burada biter. Sağlıcakla kalın.