github icontwitter icon

Test Test Diye Nice Nice: Mocklama (1) - Fonksiyon

Author imageEnes Başpınar /20 Eki 2022
21 min read •––– views

Bölümler

İçerik

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.

productApi.js

_18
import axios from "axios";
_18
_18
async function getProduct(productId) {
_18
const response = await axios.get(`https://dummyjson.com/products/${productId}`);
_18
_18
return response.data;
_18
}
_18
_18
function mapProduct(data) {
_18
const mappedData = { ...data };
_18
_18
mappedData.hotDeal = data.discountPercentage > 80;
_18
mappedData.isRunningOut = data.stock < 5;
_18
_18
return mappedData;
_18
}
_18
_18
export { getProduct, mapProduct };

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ı!

productTest.js

_33
import axios from "axios";
_33
import { getProduct, mapProduct } from "./productApi.js";
_33
_33
axios.get = async () => {
_33
return {
_33
data: {
_33
id: 999,
_33
title: "Fake iPhone 11",
_33
description: "An apple mobile which is nothing like apple",
_33
price: 549,
_33
discountPercentage: 99.99,
_33
rating: 4.69,
_33
stock: 94,
_33
brand: "Apple",
_33
category: "smartphones",
_33
thumbnail: "https://dummyjson.com/image/i/products/1/thumbnail.jpg",
_33
images: ["https://dummyjson.com/image/i/products/1/1.jpg"],
_33
},
_33
};
_33
};
_33
_33
async function test() {
_33
const product = await getProduct(3);
_33
const mappedProduct = mapProduct(product);
_33
_33
if (!mappedProduct.hotDeal) {
_33
throw new Error("mapProduct method works incorrectly. take a look at the code.");
_33
} else {
_33
console.log("mapProduct method works correctly.");
_33
}
_33
}
_33
_33
test();

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.

mockFunction.test.js

_4
test("playground", () => {
_4
const mockFunction = jest.fn();
_4
console.log("mockFunction:", mockFunction)
_4
});

=== 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.

overridePackageMethod.test.js

_6
import axios from "axios";
_6
_6
test("playground", () => {
_6
axios.get = jest.fn();
_6
console.log("mock Implementation:", axios.get.toString());
_6
});

=== Output:

mock Implementation: function () { return fn.apply(this, arguments); }

Ayrıca yerleşik nesnelerin yöntemlerini de mocklayabiliriz.

builtInMethod.test.js

_5
test("playground", () => {
_5
console.log("original Implementation:", Math.random.toString());
_5
Math.random = jest.fn();
_5
console.log("mock Implementation:", Math.random.toString());
_5
});

=== 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.
manipulateArray.test.js

_11
function manipulateArray(array, manipulateMethod) {
_11
return array.map((item) => manipulateMethod(item));
_11
}
_11
_11
test("playground", () => {
_11
const array = [0, 1, 2]
_11
const mockManipulateMethod = jest.fn((x) => x + 2);
_11
manipulateArray(array, mockManipulateMethod);
_11
_11
console.log("mock property:", mockManipulateMethod.mock);
_11
});

=== 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.
mockReturns.test.js

_11
test("playground", () => {
_11
const mockFunction = jest
_11
.fn()
_11
.mockReturnValue("other calls")
_11
.mockReturnValueOnce("first call")
_11
.mockReturnValueOnce("second call");
_11
_11
for (let index = 0; index < 5; index++) {
_11
console.log("mockedProduct", mockFunction());
_11
}
_11
});

=== 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.

getFromLocalStorage.test.js

_34
function getFromLocalStorage(key) {
_34
return window.localStorage.getItem(key);
_34
}
_34
_34
test("should get data from local storage correctly", () => {
_34
const key = "testKey";
_34
const value = "testValue";
_34
// mock fonksiyonunu oluşturalım.
_34
const mockLocalStorageGet = jest.fn();
_34
_34
// window.localStorage.getItem'a mock fonksiyonu atayalım.
_34
// böylece test etmek için istediğimiz değeri döndürebiliriz.
_34
Object.defineProperty(window, "localStorage", {
_34
value: {
_34
getItem: mockLocalStorageGet,
_34
},
_34
});
_34
_34
// window.localStorage.getItem çağrıldığında "testValue"
_34
// döndürmesini isteriz. bunun için mock fonksiyonunu da
_34
// mocklanan fonksiyonu da kullanabiliriz.
_34
mockLocalStorageGet.mockReturnValue(value);
_34
// ya da: window.localStorage.getItem.mockReturnValue(value);
_34
_34
// test edeceğimiz fonksiyonu çağıralım.
_34
getFromLocalStorage(key);
_34
_34
// fonksiyonun mocklanıp mocklanmadığına kontrol edelim.
_34
expect(jest.isMockFunction(window.localStorage.getItem)).toBe(true)
_34
// fonksiyonun "testKey" ile çağırıldığını kontrol edelim.
_34
expect(mockLocalStorageGet.mock.lastCall[0]).toBe(key);
_34
// fonksiyonun belirlediğimiz değeri döndürdüğünü kontrol edelim.
_34
expect(mockLocalStorageGet.mock.results[0].value).toBe(value);
_34
});

İ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.

getRemainingTime.test.js

_26
function getRemainingTime(endDate, startDate = new Date()) {
_26
let delta = (endDate - startDate) / 1000;
_26
_26
return {
_26
remainingDays: Math.floor(delta / (60 * 60 * 24)),
_26
remainingHours: Math.floor((delta / (60 * 60)) % 24),
_26
remainingMinutes: Math.floor((delta / 60) % 60),
_26
remainingSeconds: Math.floor(delta % 60),
_26
};
_26
}
_26
_26
test("should return remaining data when give future date", () => {
_26
const endDate = new Date(2023, 1, 1);
_26
// new Date() ile döndürülecek değeri belirleyelim'.
_26
const mockCurrDate = new Date(2022, 10, 16, 16, 9, 25);
_26
// Date constructor'ından oluşacak nesnenin mockCurrDate
_26
// olmasını sağlayalım.
_26
global.Date = jest.fn().mockReturnValue(mockCurrDate);
_26
_26
expect(getRemainingTime(endDate)).toEqual({
_26
remainingDays: 76,
_26
remainingHours: 7,
_26
remainingMinutes: 50,
_26
remainingSeconds: 35,
_26
});
_26
});

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.

manipulateArray.test.js

_11
function manipulateArray(array, manipulateMethod) {
_11
return array.map((item) => manipulateMethod(item));
_11
}
_11
_11
test("playground", () => {
_11
const array = [0, 1, 2];
_11
const manipulateMethod = jest.fn().mockImplementation(x => x + 2);
_11
manipulateArray(array, manipulateMethod);
_11
_11
console.log(manipulateMethod.mock.results);
_11
});

=== 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.

withImplementation.test.js

_16
test("playground", () => {
_16
const mockMethod = jest.fn(() => 'outside callback');
_16
_16
console.log(mockMethod());
_16
_16
mockMethod.withImplementation(
_16
// mockMethod implementasyonu belirtir.
_16
() => 'inside callback',
_16
// implementasyon sadece bu fonksiyon kapsamında geçerli olur.
_16
() => {
_16
console.log(mockMethod());
_16
},
_16
);
_16
_16
console.log(mockMethod());
_16
});

=== 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.

getProduct.test.js

_54
import axios from "axios";
_54
_54
async function getProduct(productId) {
_54
try {
_54
const response = await axios.get(`https://dummyjson.com/products/${productId}`);
_54
_54
return response.data;
_54
} catch (error) {
_54
return null;
_54
}
_54
}
_54
_54
describe("getProduct tests", () => {
_54
beforeEach(() => {
_54
axios.get = jest.fn();
_54
});
_54
_54
test("should be return product data when request is succesfully", async () => {
_54
const mockedValue = {
_54
data: {
_54
id: 1,
_54
title: "iPhone 9",
_54
description: "An apple mobile which is nothing like apple",
_54
price: 549,
_54
discountPercentage: 12.96,
_54
rating: 4.69,
_54
stock: 94,
_54
brand: "Apple",
_54
category: "smartphones",
_54
thumbnail: "https://dummyjson.com/image/i/products/1/thumbnail.jpg",
_54
images: [
_54
"https://dummyjson.com/image/i/products/1/1.jpg",
_54
"https://dummyjson.com/image/i/products/1/2.jpg",
_54
"https://dummyjson.com/image/i/products/1/3.jpg",
_54
"https://dummyjson.com/image/i/products/1/4.jpg",
_54
"https://dummyjson.com/image/i/products/1/thumbnail.jpg",
_54
],
_54
},
_54
};
_54
axios.get.mockResolvedValue(mockedValue);
_54
_54
const result = await getProduct();
_54
_54
expect(result).toStrictEqual(mockedValue.data);
_54
});
_54
_54
test("should be return product data when request is failed", async () => {
_54
axios.get.mockRejectedValue(new Error("Error occured when fetching data!"));
_54
_54
const result = await getProduct();
_54
_54
expect(result).toStrictEqual(null);
_54
});
_54
});

=== 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.

afterEachless.test.js

_17
describe("playground", () => {
_17
test("test 1", () => {
_17
Math.random = jest.fn().mockReturnValue(55);
_17
_17
console.log("first random value: ", Math.random());
_17
console.log("second random value: ", Math.random());
_17
_17
console.log(Math.random.mock.calls.length);
_17
});
_17
_17
test("test 2", () => {
_17
console.log("third random value: ", Math.random());
_17
console.log("fourth random value: ", Math.random());
_17
_17
console.log(Math.random.mock.calls.length);
_17
});
_17
});

=== 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() ve jest.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.

afterEach.test.js

_21
describe("playground", () => {
_21
afterEach(() => {
_21
Math.random.mockRestore();
_21
});
_21
_21
test("test 1", () => {
_21
Math.random = jest.fn().mockReturnValue(55);
_21
_21
console.log("first random value:", Math.random());
_21
console.log("second random value:", Math.random());
_21
_21
console.log(Math.random.mock.calls.length);
_21
});
_21
_21
test("test 2", () => {
_21
console.log("third random value:", Math.random());
_21
console.log("fourth random value:", Math.random());
_21
_21
console.log(Math.random.mock.calls.length);
_21
});
_21
});

=== 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.

dateNow.test.js

_14
test("playground", () => {
_14
const dateNowSpy = jest.spyOn(Date, "now");
_14
_14
// orijinal implementasyonu kullanır.
_14
console.log("mocked function:", Date.now);
_14
console.log("first call return value:", Date.now());
_14
_14
dateNowSpy.mockReturnValueOnce(500);
_14
_14
// mock değeri döndürür.
_14
console.log("second call return value:", Date.now());
_14
// orijinal implementasyonu hala kaybetmez ve kullanır.
_14
console.log("third call return value:", Date.now());
_14
});

=== 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.

dateNowImplementation.test.js

_11
test("playground", () => {
_11
jest.spyOn(global.Date, "now");
_11
console.log("first call return value: ", Date.now());
_11
_11
global.Date.now.mockImplementation(() => "Hacked!");
_11
console.log("second call return value: ", Date.now());
_11
console.log("third call return value: ", Date.now());
_11
_11
global.Date.now.mockRestore();
_11
console.log("fourth call return value: ", Date.now());
_11
});

=== 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.


_11
test("playground", () => {
_11
const mockLocalStorageGet = jest.fn();
_11
_11
Object.defineProperty(window, "localStorage", {
_11
value: {
_11
getItem: mockLocalStorageGet,
_11
},
_11
});
_11
_11
console.log(window.localStorage.getItem.mock.results);
_11
});

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.


_5
// kullanım 1 (favorim):
_5
const mockedLocalStorageGetItem = jest.mocked(window.localStorage.getItem)
_5
_5
// kullanım 2:
_5
const mockedLocalStorageGetItem = window.localStorage.getItem as jest.Mock;

Ö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.

_19
test("playground", async () => {
_19
const mockAreaCalculate = jest.fn((x, y) => x * y);
_19
_19
mockAreaCalculate(7, 13);
_19
mockAreaCalculate(3, 5);
_19
mockAreaCalculate(5, 8);
_19
_19
expect(mockAreaCalculate).toHaveBeenCalled();
_19
expect(mockAreaCalculate).toHaveBeenCalledTimes(3);
_19
expect(mockAreaCalculate).toHaveBeenCalledWith(7, 13);
_19
expect(mockAreaCalculate).toHaveBeenLastCalledWith(5, 8);
_19
expect(mockAreaCalculate).toHaveBeenNthCalledWith(2, 3, 5);
_19
_19
expect(mockAreaCalculate).toHaveReturned();
_19
expect(mockAreaCalculate).toHaveReturnedTimes(3);
_19
expect(mockAreaCalculate).toHaveReturnedWith(7 * 13);
_19
expect(mockAreaCalculate).toHaveLastReturnedWith(5 * 8);
_19
expect(mockAreaCalculate).toHaveNthReturnedWith(2, 3 * 5);
_19
});

=== 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.

_20
test("playground", async () => {
_20
const calledArguments = [
_20
[1, 2, 3],
_20
{
_20
name: "Enes",
_20
surname: "Başpınar",
_20
job: "Software Developer",
_20
age: 23
_20
},
_20
];
_20
_20
expect(calledArguments).toEqual([
_20
expect.arrayContaining([1, 3]),
_20
expect.objectContaining({
_20
name: expect.anything(),
_20
age: expect.any(Number),
_20
job: expect.stringContaining("Developer"),
_20
}),
_20
]);
_20
});

=== 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.

Kaynaklar

2022 © No rights are reserved.Inspired by Lee Robinson's blog.