Test Test Diye Nice Nice: Mocklama (1) - Fonksiyon
Bölümler
- Test Test Diye Nice Nice: Mocklama (1) - Fonksiyon (Şu anda buradasınız)
- Test Test Diye Nice Nice: Mocklama (2) - Modül
- Test Test Diye Nice Nice: Mocklama (3) - Timer
- Test Test Diye Nice Nice: Mocklama (4) - React
İçerik
- Mock Nedir ve Hangi Problemi Çözer?
- Mock Türleri
- Girizgah
- Mock Fonksiyonu
- Spy
- TypeScript Type Desteği
- Özel Expect Metodları
Seriye isim verirken esinlendiğim, üstat Aşık Veysel'in muazzam türküsünü şuraya iliştirip başlayalım.
Günümüzde her bir uygulama akışlardan oluşur. E-ticaret uygulamaları özelinde bakarsak ürünleri inceleyebiliriz, önerileri keşfedebiliriz, ürün satın alabiliriz ve daha nice işlemler gerçekleştirebiliriz. Tüm bu akışların doğru işlediğinden emin olmak, yazılım geliştirme sürecinin önemli bir parçasıdır. Uygulamanın tüm senaryolar karşısında beklenen davranışı sergileyeceğinden emin olmak isteriz. Ancak test yazarken genelde işler yolunda gitmez.
Yazının kodlarına Github üzerinden erişebilirsiniz.
Mock Nedir ve Hangi Problemi Çözer?
Esasen, yazılımdaki herhangi bir şeyin taklidine mock diyebiliriz.
İndirimli ürünlerin fiyatının doğru gösterilmesi bir test senaryomuz olabilir. Testi yazdığımız esnada indirime sahip ürün bulup onunla ilerleyebilir ve testin geçmesinin mutluluğuyla merge request'i açabiliriz. Ancak bir gün pipeline çalıştığında test adımının tamamlanamadığına şahit oluruz. İndirim elbette ki sonsuza kadar sürmez ve süresi dolduğunda işler beklendiği gibi gitmez, test başarısız olur.
Diğer bir test senaryosu ise ürün satın alım akışının doğru çalışıp çalışmadığıdır. Bu senaryo daha karmaşıktır ve daha kritiktir. Ödeme yapılır, bankaya istek atılır, işlem gerçekleşir, sipariş iptal edilir ve paranın geri yatması beklenir. Ve bu süreç binlerce kez tekrarlanır. Ortaya büyük bir kaos çıkar.
İşlemi gerçekleştiren kodu test etmeyi beklerken aynı zamanda Checkout API'sini ve hatta banka API'sini de test eder vaziyete geliriz. Testlerin odağını kaybetmemesi prensibi gereği API'den sahte bir cevap geldiğini varsaysaydık yalnızca kendi fonksiyonumuzu test edebilirdik. Arkamızda laf edecek sinirli bir satıcı ve bankacı güruhu da bırakmazdık.
Buradan konumuza bağlamak gerekirse; verileri, foksiyonları ve bilimum yazılım araçlarının yerine konulmuş taklidine mock denir.
Mock Türleri
Test ile yeteri kadar uğraştıysanız mock yerine stub, fake, dummy gibi bir sürü kelime kullanıldığına denk gelmişsinizdir. Gerard Meszaros bir kitabında bu kargaşayı önlemek için gerçek nesnenin yerine konulacak sahte nesneleri Test Double yani test dublörü olarak ifade eder. Oyuncuların yerine geçen dublörler gibisine. Test dublörünü de altı alt kategoriye ayırır:
- Dummy: Fonksiyona geçmemiz gereken parametreleri doldurmak için kullanılır ancak gerçekte bir işe yaramaz.
- Fake: Kodun basitleştirilmiş ancak çalışan bir halini içerir. Buna verebileceğimiz örnek şifrelerin hashlerini almak için veritabanına gitmek yerine test verisi tutan bir nesneden okumaktır.
- Stub: Test sırasındaki fonksiyon çağrılarında planlanmış yanıtlar döndürmek için kullanılır. İlk çağrıldığında A sonucunu ikincisinde B sonucunu diğerlerinde C sonucunu döndür diyebiliriz.
- Spy: Fonksiyonların kaç kez ve hangi parametreler ile çağrıldığı, ve hangi cevapları döndüğü gibi ekstra bilgileri kaydeder.
- Mock: Belirli argümanlarla çağrılmasını beklediğimiz fonksiyonların
döndüreceği sonuçları planlamak için kullanılır. Beklenmedik argümanlarla çağrıldığında ise hata fırlatır.
Pratikte bunlar iç içe girmiştir ve ayrım yapmakta zorlanırız. Bu sebeple mock diye genelliyor olacağız.
Girizgah
Fonksiyonları mocklama motivasyonumuz, orijinal lojiği manipüle ederek istediğimiz değerleri döndürmeye zorlamaktır. Bunların yanı sıra çağrılara ait detayların geçmişini tutar. Böylece fonksiyon çağırılma sayısını, kaç kere çağırıldığını ve hangi parametrelerle çağırıldığını test edebiliriz.
Jest'e geçmeden önce fonksiyon mocklama işlemini kod üzerinde görselleştirelim. Ürünün bilgisini çeken ve manipüle eden yardımcı fonksiyonlarımız olduğunu varsayalım.
Bu kodlar üzerinde test etmek istediğimiz senaryo ise indirim yüzdesinin %80 üzerinde olması halinde "Sıcak Teklif" olarak tanımlanması olsun. discountPercentage
değerinin istediğimiz gibi olacak şekilde sahte bir ürün dönmesini isteriz. O zaman manipülasyon zamanı!
Kodu manuel olarak istediğimiz yönde şekillendiririz. Diğer bir ifadeyle ünlü Türk kimyager Abuzer Kömürcü'nün de dediği gibi ezeriz.
Böylece gelen ürünün istediğimiz bilgileri içermediği ve API'ye erişilemediği durumlar karşısında testimiz kırılgan hale gelmemiş olur. Aynı zamanda API'nin cevap süresiyle de test süremizi uzatmayız. Mocklamanın arkasındaki temel mantık budur.
Mock Fonksiyonu
Artık Jest ile devam edebiliriz. Mock fonksiyonu oluşturmak için jest.fn(implementation?)
metodu kullanılır.
=== Output:
mockFunction: [Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
withImplementation: [Function: bound withImplementation],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
}
Mocklamak istediğimiz metodları bu mock fonksiyon ile ezeriz.
=== Output:
mock Implementation: function () {
return fn.apply(this, arguments);
}
Ayrıca yerleşik nesnelerin yöntemlerini de mocklayabiliriz.
=== Output:
original Implementation: function random() { [native code] }
mock Implementation: function () {
return fn.apply(this, arguments);
}
Peki mock fonksiyonun büyüsü nedir?
.mock Niteliği
Mock fonksiyonları, testlerde işimizi kolaylaştıracak bilgileri kaydederler.
Bunlara mock
özelliği ile erişebiliriz. Niteliklerine bakacak olursak:
mock.calls
- Fonksiyonun çağrıldığı argümanları listeler.mock.results
- Fonksiyon çağrılarının sonucunu listeler. Dönüş türü, return, throw ya da incomplete olabilir.mock.instances
- Fonksiyon constructor ise üretilen nesneleri listeler.mock.contexts
- Fonksiyon çağrıldığı andaki this nesnelerini listeler.mock.lastCall
- Fonksiyonun son çağırıldığı argümanları listeler.
=== Output:
mockManipulateMethod's mock property: {
"calls": [[0], [1], [2]],
"contexts": [null, null, null],
"instances": [null, null, null],
"results": [
{ "type": "return", "value": 2 },
{ "type": "return", "value": 3 },
{ "type": "return", "value": 4 },
],
"lastCall": [2]
}
Makalenin devamında bu özelliği aktif olarak kullanacağız.
Statik Değer Döndürme
Mock metodlarının senaryoya uygun değerler üretmesi beklenir. Buna yönelik statik değerler döndürmek için iki metoda sahibiz:
mockReturnValue(value)
- Tüm çağrılarda döndürülecek değeri belirler.mockReturnValueOnce(value)
- Tek seferliğine döndürülecek değeri belirler.
=== Output:mockedProduct first call mockedProduct second call mockedProduct other calls mockedProduct other calls mockedProduct other calls
İki örneğe bakalım.
İlk örnekte localStorage
'den okuduğu değeri döndüren bir fonksiyonumuz
olduğunu düşünelim. Verdiğimizde anahtara göre hafızadan okuduğu değeri
döndürüp döndürmeyeceğini test etmek için window.localStorage.getItem
metodunu mocklayalım.
İkinci örnekte ise gelecekteki bir tarihe kalan süreyi hesaplamak istiyoruz.
Eğer mocklamazsak o anki tarihi baz alır. Zamanın akışına kapılıp bitiş
tarihi geçtiğimizde test patlamaya başlayacaktır. İşte bu sebeple
new Date()
'i mocklayıp döndürdüğü tarihin bitiş tarihinden önce olduğundan
emin olmalıyız.
Birim testlerin mantığı gereği test etmek istenen fonksiyonda kullanılan harici fonksiyonlar (yerleşik ya da import edilmiş olması farketmeksizin) mocklanmalıdır.
Dinamik Değer Döndürme
Argümanlara göre dinamik değer döndürmek istenebilir. Bunun için mock fonksiyona implementasyon tanımlamak için üç metoda sahibiz:
mockImplementation(func)
- Tüm çağrılışlarında kullanılacak implementasyonu belirler.mockImplementationOnce(func)
- Tek seferliğine kullanılacak implementasyonu belirler.withImplementation(func, callback)
- Verilen callback fonksiyonun scope'u içerisinde çağırıldığında döneceği değeri belirler.
İki örneğe bakalım.
İlk örnekte verilen fonksiyonu dizinin elemanlarına uygulayan bir fonksiyonun implementasyonunu değiştirelim.
=== Output:
[
{ type: 'return', value: 2 },
{ type: 'return', value: 3 },
{ type: 'return', value: 4 }
]
İkinci örnekte ise belirli blok içerisinde implementasyonu nasıl değiştirebileceğimizi görelim.
=== Output:outside callback inside callback outside callback
Buraya kadarki kısmın anlaşılması önemliydi. Serinin geri kalanı genelde impelementasyonun ve dönüş değerinin mocklanması etrafında dönecek.
Asenkron Fonksiyonları Mocklama
Asenkron fonksiyonlar bildiğimiz gibi Promise döndürürler. Dolayısıyla
mock fonksiyonumuzun da Promise döndürmesini bekleriz. Cebimizdeki mockImplementation()
metoduyla bunu
jest.fn().mockImplementation(() => Promise.resolve(value));
ya da
jest.fn().mockImplementation(() => Promise.reject(value));
olacak şekilde
yapabiliriz. Ancak Jest bu implementasyonları abstract eden dört metod sağlar:
mockResolvedValue(value)
- Tüm çağrılarda resolve edilmiş sonuç döndürür.mockResolvedValueOnce(value)
- Tek seferliğine resolve edilmiş sonuç döndürür.mockRejectedValue(value)
- Tüm çağrılarda reject edilmiş sonuç döndürür.mockRejectedValueOnce(value)
- Tek seferliğine reject edilmiş sonuç döndürür.
Ürün bilgilerini API'den çeken bir metodun doğru veri döndürdüğünü ve işlem sırasında hata aldığında ise null
döndürdüğünü test edelim.
=== Output:PASS src/tests/getProduct.test.js
getProduct tests
✓ should be return product data when request is succesfully (20 ms)
✓ should be return product data when request is failed (1 ms)
Mock Verilerini Temizleme
Bazı zamanlar fonksiyonlarımız birden fazla senaryoya sahip olabilir. Jest mock fonksiyonlarının verilerini kendiliğinden temizlemez. Dolayısıyla beklenmedik durumlara denk gelebiliriz.
=== Output:first random value: 55 second random value: 55 2 third random value: 55 fourth random value: 55 4
Testin içerisinde yaptığımız mock işleminin diğerini de etkilediğini görebiliriz. Çünkü direkt olarak import edilen nesneyi değiştirmiş olduk. Halbuki geçmişin test bazlı tutulmasını isteriz. Bunun için üç farklı metoda sahibiz:
mockClear()
- .mock özelliğindeki verileri temizler.mockReset()
- mockClear() metoduna ek olarak mockReturnValue ve mockImplementation türevi fonksiyonların etkilerini temizler.mockRestore()
- mockReset() metoduna ek olarak mock, Spy ile oluşturulduysa, orjinal implementasyonu geri yükler.
Tüm mock fonksiyonların verilerini temizlemek için
jest.clearAllMocks()
,jest.resetAllMocks()
vejest.restoreAllMocks()
kullanılabilir. Ayrıca her test dosyasına ayrı ayrı yazmamak adına jest.config.js dosyasında clearMocks, resetMocks ve restoreMocks kuralları etkinleştirilebilir.
Bu metodların afterEach
ile kullanılması güzel bir pratiktir. Böylece her
testten sonra temizlik yapıldığından emin oluruz. Testi güncelleyelim.
=== Output:first random value: 55 second random value: 55 2 third random value: undefined fourth random value: undefined 2
Değerlerin undefined
dönmesi mocklanan metodun implementasyonunun olmadığını
ve varsayılan implemantasyon olan () => undefined
fonksiyonunu kullandığını
gösterir. Görünüşe göre mockRestore
işe yaramadı. Peki neden?
Spy
Metodları ezerek mocklarsak orijinal implementasyona erişimi kaybederiz. Bu problemi çözmek için
jest.spyOn(object, methodName)
kullanılır.
Varsayılan olarak fonksiyonun orijinal implementasyonuna dokunulmaz ancak
jest.fn()
gibi çağrıları takip edebilmemizi sağlar. Döndürülen değer aynı
zamanda mock fonksiyonudur ve şu ana kadarki tüm yöntemleri kullanabileceğimizi
gösterir.
=== Output:
mocked function: [Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
withImplementation: [Function: bound withImplementation],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
}
first call return value: 1671971862021
second call return value: 500
third call return value: 1671971862027
İmplementasyonu ezmek istersek kullanmamız gereken metodları halihazırda biliyoruz.
=== Output:first call return value: 1671972138129 second call return value: Hacked! third call return value: Hacked! fourth call return value: 1671972138151
Hangisini kullanacağınız konusunda kafanız karıştıysa özet geçelim.
Eğer bir fonksiyonun sadece kaç kez ve hangi parametrelerle çağrıldığını
izlemek istiyorsak ve bunları yaparken de lojiğini bozmak istemiyorsak
jest.fn()
yerine jest.spyOn()
kullanabiliriz. Aksi durumda hangisini
seviyorsak onunla yardırabiliriz.
TypeScript Type Desteği
Typescript projelerinde mock fonksiyonların özel niteliklerine erişirken tip hatasıyla karşılaşırız. Örnekteki kodda mock
isimli bir özelliğinin olmadığına dair bir hata alırız.
Problemi çözmek için jest.Mock
ya da jest.mocked()
kullanabilirsiniz.
jest.mocked verdiğiniz nesnenin içerisindeki tüm mock fonksiyonlara gerekli tipleri ekler.
Özel Expect Metodları
.mock
özelliğini testlerimizde direkt kullanmayız. Bu özelliği kullanmak üzere Jest'in yardımcı metodlarını kullanırız.
.toHaveBeenCalled()
- En az bir kez çağrılıp çağrılmadığını kontrol eder..toHaveBeenCalledTimes(n)
- N kez çağırılıp çağırılmadığını kontrol eder..toHaveBeenCalledWith(...args)
- Spesifik argümanlarla en az bir kez çağırılıp çağrılmadığını kontrol eder..toHaveBeenLastCalledWith(...args)
- Sonuncu çağrılışında spesifik argümanlarla çağırılıp çağrılmadığını kontrol eder..toHaveBeenNthCalledWith(nthCall, ...args)
- N. çağrılışında spesifik argümanlarla çağırılıp çağrılmadığını kontrol eder..toHaveReturned()
- En az bir kez başarılı (error fırlatmıyorsa) değer döndürüp döndürmediğini kontrol eder..toHaveReturnedTimes(number)
- N kez başarılı değer döndürüp döndürmediğini kontrol eder..toHaveReturnedWith(value)
- En az bir kez verdiğimiz değeri döndürüp döndürmediğini kontrol eder..toHaveLastReturnedWith(value)
- Sonuncu çağrılışında verdiğimiz değeri döndürüp döndürmediğini kontrol eder..toHaveNthReturnedWith(nthCall, value)
- N. çağrılışında verdiğimiz değeri döndürüp döndürmediğini kontrol eder.
=== Output:PASS playground.test.ts ✓ playground
Çağrıldığı argümanları ya da döndürdüğü değerleri kısmi olarak test etmek istersek expect
özellikleri kullanabiliriz.
expect.anything()
- Null ve undefined dışında her tür değerle eşleşir.expect.any(constructor)
- İlgili constructor ile oluşturulmuş değerlerle eşleşir. eşleşir.expect.stringContaining(string)
- Verilen string'i içeriyorsa eşleşir.expect.stringMatching(string | regexp)
- Verilen string ya da regex ile eşleşiyorsa eşleşir.expect.arrayContaining(array)
- Dizi verilen alt kümeyi içeriyorsa eşleşir.expect.objectContaining(object)
- Obje verilen alt objeyi içeriyorsa eşleşir.expect.not.stringContaining(string)
- String değilse ya da verilen string'i içermiyorsa eşleşir.expect.not.stringMatching(string | regexp)
- String değilse ya da verilen string ve regex ile eşleşmiyorsa eşleşir.expect.not.arrayContaining(array)
- Dizi verilen alt kümeyi içermiyorsa eşleşir.expect.not.objectContaining(object)
- Obje verilen alt objeyi içermiyorsa eşleşir.
=== Output:PASS playground.test.ts ✓ playground
Kapanış
Test yazmak ve özellikle fonksiyon mocklamak bu alana yeni başlayan insanları zorlayabiliyor. Bu sebeple yeni bir seri başlatmak istedim. Geri bildirimlerinizi bekliyorum. Bitirirken de konuya dair yaptığım bir caps'i paylaşmak istiyorum.
Yazı burada biter. Sağlıcakla kalın.