Bölümler
İçerik
Önceki yazıda mock kavramının ne olduğuna ve hangi ihtiyaçtan doğduğuna
değindik. Kütüphane ve built-in metodların mocklanış yöntemlerinden bahsettik.
Artık modüllere böldüğümüz fonksiyonları nasıl mocklayabileceğimize göz atalım.
Yazının kodlarına Github üzerinden erişebilirsiniz.
Modül Nedir?
Uygulamalar büyüdükçe dosyalara bölme ihtiyacı hissedilmeye başlanır. Fonksiyon
gruplarını içeren bu dosyalara modül denir. Mocklarken hangi yöntemleri
kullanabileceğimiz anlayabilmek için içe aktarılan dosyaların içeriklerine
(nesne mi? değerleri neler?) göz atacağız.
CommonJs kullanarak yapabileceğimiz farklı import çeşitleri:
Dosyaların içeriklerini görmek için sekmeler arasında geçiş yapabilirsiniz.
_7const utils = require("./utils");
_7const { getProduct } = require("./utils");
_7test("playground", () => {
_7 console.log("require with default:", utils);
_7 console.log("require with partial :", getProduct);
=== Output:require with default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct],
default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct]
}
}
require with partial : [Function: getProduct]
ES Modules kullanarak yapabileceğimiz farklı import çeşitleri:
_8import * as utilsWithStar from "./utils";
_8import utilsWithDefault, { getProduct } from "./utils";
_8test("playground", () => {
_8 console.log("import with * as:", utilsWithStar);
_8 console.log("import with default:", utilsWithDefault);
_8 console.log("import with partial:", getProduct);
=== Output:import with * as: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct],
default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct]
}
}
import with default: {
get: [Function: get],
getUser: [Function: getUser],
getProduct: [Function: getProduct]
}
import with partial: [Function: getProduct]
Farklı içe aktarma yöntemlerinin farklarına sıradaki başlıkta değineceğiz.
Girizgah
Uygulamamızda ürün ve kullanıcı bilgilerini çekmek için yardımcı fonksiyonlara
sahip olduğumuzu varsayalım.
Kod kümelerimizi yardımcı metodlara çıkarma işlemine abstraction denir.
Örneğin ürün bilgisini çekmek görevini ifa eden kodları getProduct()
isimli
bir fonksiyona yerleştirebiliriz. Koda bakan birisi bu fonksiyonunun ne
döndüreceğini isminden anlayabilmelidir. Abstraction'ı sevelim çünkü test
yazılmasını inanılmaz derecede kolaylaştırır.
_21import axios from "axios";
_21const API_URL = "https://dummyjson.com";
_21export async function get(apiUrl: string): Promise<any> {
_21 const response = await axios.get(apiUrl);
_21 return response.data;
_21export function getProduct(productId: number): Promise<any> {
_21 return get(`${API_URL}/products/${productId}`);
_21export function getUser(userId: number): Promise<any> {
_21 return get(`${API_URL}/users/${userId}`);
Elimizdeki bilgilerle mocklamayı deneyelim. İlk akla gelen import edip ezerek
override etmek olabilir.
_7import { get } from "./index";
_7test("should be mock", () => {
_7 expect(jest.isMockFunction(get)).toBe(true);
=== Output:error TS2632: Cannot assign to 'get' because it is an import.
Mocklamaya çalıştığımız fonksiyonun import olduğuna dair uyarı ile karşılaştık.
Dosyanın nesne olarak import edilmesinin çözüm olacağını düşünebiliriz.
_9import * as UtilsModule from "./index";
_9// `import Utils from "./index"` yapmayı denerseniz default
_9// export'a sahip olmadığına dair uyarı alırsınız.
_9test("should be mock", () => {
_9 UtilsModule.get = jest.fn();
_9 expect(jest.isMockFunction(UtilsModule.get)).toBe(true);
=== Output:error TS2540: Cannot assign to 'get' because it is a read-only property.
Ancak alnımızın çatına TypeScript hatasını yeriz. Olsun, güzel denemeydi. Zaten
import'un direkt ezmek güzel bir uygulama olmazdı. Bu işi Jest'in maharetli
ellerine bırakalım.
Modül Mocklama
Modülleri mocklamak için jest.mock(relativeFilePath, factory, options)
kullanılır.
Yalnızca dosya yolu verilmişse export edilen tüm metodları otomatik mocklar.
_7import * as utils from "./utils";
_7test("playground", () => {
_7 console.log("utils Module:", utils);
=== Output:utils Module: {
__esModule: true,
getProduct: [Function: getProduct] {
_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)]
},
getUser: [Function: getUser] {
_isMockFunction: true,
...
},
default: [Function: get] {
_isMockFunction: true,
...
}
}
Dosya yolunu mockladığımızda kendisini çağıran import'lar takip edilerek sahte
modül döndürülür. Ancak unutmayalım ki test dosyasının içindeki modül çağrısı
da mocklanmış olur.
_7import * as UtilsModule from "./utils";
_7test("playground", () => {
_7 expect(jest.isMockFunction(UtilsModule.get)).toBe(true);
=== Output:PASS src/utils.test.ts
✓ playground
Fonksiyonları kullanmadan önce mocklamıştık. Modüllerin de import
edilmeden önce mocklanması gerekmez mi?
Çok güzel bir noktaya parmak bastın. Herhangi bir nesnenin kullanılmadan önce
mocklanması zorunludur. Fakat biliyoruz ki import
ifadelerinin dosyanın en
üstünde olması JS yazılımcılarının genel alışkanlığıdır. Jest'de bu yapıyı
bozmamak adına jest. mock
ifadelerini hoist eder yani import'un üzerine
taşır.
Jest'in sınırlarını keşfetmeye devam edelim.
Modül Fonksiyonların İmplementasyonunun Mocklanması
Mocklanan modüllerdeki fonksiyonlara varsayılan olarak jest.fn()
atanır.
Dolayısıyla implementasyonu ya da dönüş değerini önceki yazıdaki yöntemlerle
mocklayabiliriz.
Utils modülümüzün testlerine get
ile başlayalım. İçerisinde axios paketinin
bir metodu kullanılmıştır ve bu paketi yani modülü mocklamamız gerekir.
_61import axios from "axios";
_61import * as UtilsModule from "./utils";
_61// axios paketini mockluyoruz.
_61// jest metodlarının tiplerini modül fonksiyonlarına sarıyoruz.
_61const mockedAxios = jest.mocked(axios);
_61describe("utils tests", () => {
_61 jest.clearAllMocks();
_61 describe("get() tests", () => {
_61 test("should return product when request is success", async () => {
_61 // fonksiyonumuzun verdiğimiz parametreyi axios.get'e doğru
_61 // şekilde ilettiğini test etmek için url'i değişkene atıyoruz.
_61 const apiUrl = "https://dummyjson.com/product/1";
_61 // sahte ürünümüzü oluşturuyoruz.
_61 const mockProduct = {
_61 description: "An apple mobile which is nothing like apple",
_61 discountPercentage: 12.96,
_61 category: "smartphones",
_61 // test ettiğimiz fonksiyon içerisindeki farklı pakete bağımlı
_61 // olan axios.get fonksiyonunun çağrıldığında mock veri ile
_61 // resolve olmasını sağlıyoruz.
_61 mockedAxios.get.mockResolvedValueOnce({
_61 // test edeceğimiz fonksiyonu çağırıyoruz.
_61 const result = await UtilsModule.get(apiUrl);
_61 // axios.get'in ilettiğimiz url ile çağrıldığını test ediyoruz
_61 expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
_61 // istek başarısız olduğu zaman null döndürdüğünü test ediyoruz.
_61 expect(result).toStrictEqual(mockProduct);
_61 test("should return null when request is failed", async () => {
_61 const apiUrl = "https://dummyjson.com/product/1000";
_61 mockedAxios.get.mockRejectedValueOnce(
_61 new Error("Error occured when fetching data!")
_61 const result = await UtilsModule.get(apiUrl);
_61 expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
_61 expect(result).toBeNull();
=== Output:PASS src/tests/utils.test.ts
utils tests
get() tests
✓ should return product whe request is success
✓ should return null when request is failed
Jest, modülleri mockladıktan sonra kendi metodları için TypeScript tiplerini
eklemez. Barındırdığı tüm fonksiyonlara Jest metodlarının tiplerini sarmalamak
istersek jest.mocked(source)
metodunu kullanabiliriz.
Factory Parametresi
Modül mocklanırken fonksiyonların otomatik mocklanmasını istemiyorsak ve
kendimiz konfigüre edeceksek factory
parametresini kullanabiliriz.
Örneğimizde eğer dönüş değeri mocklanmamışsa reject edecek bir yapı kuralım
ve ikinci testi refactor edelim.
_53import axios from "axios";
_53import * as UtilsModule from "./utils";
_53jest.mock("axios", () => {
_53 .mockRejectedValue(new Error("Error occured when fetching data!")),
_53const mockedAxios = jest.mocked(axios);
_53describe("utils tests", () => {
_53 jest.clearAllMocks();
_53 describe("get() tests", () => {
_53 test("should return product when request is success", async () => {
_53 const apiUrl = "https://dummyjson.com/product/1";
_53 const mockProduct = {
_53 description: "An apple mobile which is nothing like apple",
_53 discountPercentage: 12.96,
_53 category: "smartphones",
_53 mockedAxios.get.mockResolvedValueOnce({
_53 const result = await UtilsModule.get(apiUrl);
_53 expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
_53 expect(result).toStrictEqual(mockProduct);
_53 test("should return null when request is failed", async () => {
_53 const apiUrl = "https://dummyjson.com/product/1000";
_53 const result = await UtilsModule.get(apiUrl);
_53 expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
_53 expect(result).toBeNull();
=== Output:PASS src/tests/utils.test.ts
utils tests
get() tests
✓ should return product whe request is success
✓ should return null when request is failed
Peki ne zaman factory
kullanmalıyız?
Fonksiyonların mocklamaları testler boyunca aynı kalacaksa, default
mock değeri tanımlamamız gerekiyorsa ya da kısmi mocklama yapacaksak
kullanabiliriz.
Dikkat etmemiz gereken önemli bir nokta, factory ve test içerisinde
mockların ahengidir. Lafı uzatmadan şu üç farklı kullanımı özümseyelim.
_17import axios from "axios";
_17jest.mock("axios", () => {
_17 get: jest.fn().mockResolvedValue("Mock in module factory"),
_17const mockedAxios = jest.mocked(axios);
_17test("playground", async () => {
_17 const apiUrl = "https://dummyjson.com";
_17 mockedAxios.get.mockResolvedValue("Mock in test");
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
_17import axios from "axios";
_17jest.mock("axios", () => {
_17 get: jest.fn().mockResolvedValue("Mock in module factory"),
_17const mockedAxios = jest.mocked(axios);
_17test("playground", async () => {
_17 const apiUrl = "https://dummyjson.com";
_17 mockedAxios.get.mockResolvedValueOnce("Mock in test");
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in module factory
_17import axios from "axios";
_17jest.mock("axios", () => {
_17 get: jest.fn().mockResolvedValueOnce("Mock in module factory"),
_17const mockedAxios = jest.mocked(axios);
_17test("playground", async () => {
_17 const apiUrl = "https://dummyjson.com";
_17 mockedAxios.get.mockResolvedValue("Mock in test");
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in module factory
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
_17import axios from "axios";
_17jest.mock("axios", () => {
_17 get: jest.fn().mockResolvedValueOnce("Mock in module factory"),
_17const mockedAxios = jest.mocked(axios);
_17test("playground", async () => {
_17 const apiUrl = "https://dummyjson.com";
_17 mockedAxios.get.mockResolvedValueOnce("Mock in test");
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in module factory
_17 console.log(await mockedAxios.get(apiUrl)); // Output: Mock in test
Kısmi Mocklama
Şimdiye kadar daima tüm modülü mockladık. Ancak yalnızca bazı işlevleri mocklamak isteyebiliriz. Modüllerin gerçek içeriğine erişmek için jest.requireActual
kullanılır.
_9jest.mock('../moduleName', () => {
_9 const originalModule = jest.requireActual('../moduleName');
_9 functionThatBeMock: jest.fn(),
Spesifik Testlerde Modül Mocklamak
get
fonksiyonun testlerini yazdık. Dilerseniz getProduct
testine geçelim.
Test ettiğimiz foksiyonun bağımlı olduğu diğer fonksiyonları mocklamak iyi bir
pratiktir. Bu sebeple getProduct
içerisinde kullanılan get
fonksiyonunu
mocklayacağız.
_44import axios from "axios";
_44import * as UtilsModule from "./utils";
_44jest.mock("axios", () => {
_44 .mockRejectedValue(new Error("Error occured when fetching data!")),
_44const mockedUtils = jest.mocked(UtilsModule);
_44const mockedAxios = jest.mocked(axios);
_44describe("utils tests", () => {
_44 describe("getProduct() tests", () => {
_44 test("should call get func with api product endpoint when given product id", () => {
_44 const mockProduct = {
_44 description: "An apple mobile which is nothing like apple",
_44 discountPercentage: 12.96,
_44 category: "smartphones",
_44 mockedUtils.get.mockResolvedValue(mockProduct);
_44 const result = UtilsModule.getProduct(productId);
_44 expect(UtilsModule.get).toHaveBeenCalledWith(
_44 `https://dummyjson.com/products/${productId}`
_44 expect(result).toStrictEqual(mockProduct);
=== Output:utils tests
get() tests
✕ should return product whe request is success (4 ms)
✕ should return null when request is failed
getProduct() tests
✕ should call get func with api product endpoint when given product id
● utils tests › get() tests › should return product whe request is success
Expected: "https://dummyjson.com/product/1"
Number of calls: 0
● utils tests › get() tests › should return null when request is failed
Expected: "https://dummyjson.com/product/1000"
Number of calls: 0
● utils tests › getProduct() tests › should call get func with api product endpoint when given product id
Expected: "https://dummyjson.com/products/1"
Number of calls: 0
Hoppalaa... Yeni testimiz patladı, üstüne üstlük geçen testlerde
patlamaya başladı. Çıktıyı incelediğimizde, mockladığımız fonksiyonların
çağırılmadığını anlıyoruz. Jest bize, "Sen bi değerle çağırılmasını bekliyordun
fakat bu fonksiyon hiç çağırılmadı." diyor.
getProduct
fonksiyonunun implementasyonunu mocklamak için Utils
modülünü
mockladık. Ancak testi için orijinal implementasyonuna ihtiyaç duyduğumuz get
fonksiyonu da mocklanmış oldu. Test bazlı fonksiyon mocklamak işimize yarardı.
Bu problemi birkaç farklı yöntemle çözebiliriz.
jest.doMock()
Jest'te modül mocklamanın diğer bir yolu jest.doMock()
kullanmaktır. Bunun
jest.mock
'tan farkı, hoist edilmiyor olmasıdır. Yani sadece kendisinden sonra
yazılan importları mocklar.
Mocklanmasını beklediğiniz bir modül mocklanmamışsa import
edildikten sonra mocklanmış olma ihtimali çok yüksektir. Kontrol
etmeyi unutmayın.
_53import axios from "axios";
_53import UtilsModule from "./utils";
_53jest.mock("axios", () => {
_53 get: jest.fn().mockRejectedValue(new Error("Error occured when fetching data!")),
_53const mockedAxios = jest.mocked(axios);
_53describe("utils tests", () => {
_53 jest.clearAllMocks();
_53 // bu dosyadaki tüm modül mockları temizler.
_53 describe("getProduct() tests", () => {
_53 test("should call get func with api product endpoint when given product id", () => {
_53 const mockProduct = {
_53 description: "An apple mobile which is nothing like apple",
_53 discountPercentage: 12.96,
_53 category: "smartphones",
_53 jest.doMock("./utils", () => ({
_53 ...jest.requireActual("./utils"),
_53 get: jest.fn().mockResolvedValue(mockProduct),
_53 // burası kritik bir nokta. doMock hoist edilmeyeceği
_53 // için sonrasında yaptığımız require'ı kullanmalıyız.
_53 const GetModule = require("./utils");
_53 const UtilsModule = require("./utils");
_53 const result = UtilsModule.getProduct(productId);
_53 expect(GetModule.default).toHaveBeenCalledWith(`https://dummyjson.com/products/${productId}`);
_53 expect(result).toStrictEqual(mockProduct);
get
metodunu mocklayamadığımıza dair bir hata aldık. getProduct ve get aynı
dosyada yer aldığı ve biz mocklamak için import beklediğimizden dolayı başarısız
olur. Teyit etmek için mockedUtils
değişkenini yazdıralım.
_7test("should call get func with api product endpoint when given product id", () => {
_7 const UtilsModule = require("./utils");
_7 console.log("get:", UtilsModule.get);
=== Output:get: [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)]
}
Şimdi getProduct
içinde get
fonksiyonunu yazdıralım.
_4export function getProduct(productId: number): Promise<any> {
_4 console.log("get Function: ", get.toString());
=== Output:get Function: [Function: get]
Düşündüğümüz gibi fonksiyon mocklanmamış. Ayrı bir dosyaya çıkarıp yeni dosya
üzerinden mocklayabiliriz.
_83import axios from "axios";
_83import * as GetModule from "./get";
_83jest.mock("axios", () => {
_83 get: jest.fn().mockRejectedValue(new Error("Error occured when fetching data!")),
_83const mockedAxios = jest.mocked(axios);
_83describe("utils tests", () => {
_83 jest.clearAllMocks();
_83 jest.resetModules(); // clears all module mocks in this file.
_83 describe("get() tests", () => {
_83 test("should return product when request is success", async () => {
_83 const apiUrl = "https://dummyjson.com/product/1";
_83 const mockProduct = {
_83 description: "An apple mobile which is nothing like apple",
_83 discountPercentage: 12.96,
_83 category: "smartphones",
_83 mockedAxios.get.mockResolvedValueOnce({
_83 const result = await GetModule.default(apiUrl);
_83 expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
_83 expect(result).toStrictEqual(mockProduct);
_83 test("should return null when request is failed", async () => {
_83 const apiUrl = "https://dummyjson.com/product/1000";
_83 const result = await GetModule.default(apiUrl);
_83 expect(mockedAxios.get).toHaveBeenCalledWith(apiUrl);
_83 expect(result).toBeNull();
_83 describe("getProduct() tests", () => {
_83 test("should call get func with api product endpoint when given product id", async () => {
_83 const mockProduct = {
_83 description: "An apple mobile which is nothing like apple",
_83 discountPercentage: 12.96,
_83 category: "smartphones",
_83 jest.doMock("./get", () => {
_83 default: jest.fn().mockResolvedValue(mockProduct),
_83 const GetModule = require("./get");
_83 const UtilsModule = require("./utils");
_83 const result = await UtilsModule.getProduct(productId);
_83 expect(GetModule.default).toHaveBeenCalledWith(`https://dummyjson.com/products/${productId}`);
_83 expect(result).toStrictEqual(mockProduct);
=== Output:utils tests
get() tests
✓ should return product whe request is success (4 ms)
✓ should return null when request is failed
getProduct() tests
✓ should call get func with api product endpoint when given product id
jest.spyOn()
Eski bir dostla karşılaşmış gibiyiz. Evet, modüllerde de jest.spyOn()
kullanabiliriz. Ayrı dosyaya çıkartmak bu yöntemle de gerekli olsa bile
çok daha temiz bir kullanım sağlar.
_43import axios from "axios";
_43import * as GetModule from "./get";
_43import * as UtilsModule from "./utils";
_43jest.mock("axios", () => {
_43 .mockRejectedValue(new Error("Error occured when fetching data!")),
_43const mockedAxios = jest.mocked(axios);
_43describe("utils tests", () => {
_43 describe("getProduct() tests", () => {
_43 test("should call get func with api product endpoint when given product id", async () => {
_43 const mockProduct = {
_43 description: "An apple mobile which is nothing like apple",
_43 discountPercentage: 12.96,
_43 category: "smartphones",
_43 jest.spyOn(GetModule, "default").mockResolvedValue(mockProduct);
_43 const result = await UtilsModule.getProduct(productId);
_43 expect(GetModule.default).toHaveBeenCalledWith(
_43 `https://dummyjson.com/products/${productId}`
_43 expect(result).toStrictEqual(mockProduct);
=== Output:utils tests
get() tests
✓ should return product whe request is success (4 ms)
✓ should return null when request is failed
getProduct() tests
✓ should call get func with api product endpoint when given product id
Modül Mockunu Temizleme
Modülleri mockladık ancak mockları temizlemek de isteyebiliriz. Bunun için iki
methoda sahibiz. jest.dontMock()
kendinden sonraki import nesnelerinin
mocklarını temizler.
_17test("playground", () => {
_17 const axiosInstance1 = require("axios"); // mocked
_17 "Is axiosInstance1.get mocked:",
_17 jest.isMockFunction(axiosInstance1.get)
_17 jest.dontMock("axios");
_17 const axiosInstance2 = require("axios"); // unmocked
_17 "Is axiosInstance2.get mocked:",
_17 jest.isMockFunction(axiosInstance2.get)
=== Output:Is axiosInstance1.get mocked: true
Is axiosInstance2.get mocked: false
jest.unmock()
ise bulunduğu kod bloğundaki ilgili tüm import nesnelerinin
mocklarını temizler.
_17test("playground", () => {
_17 const axiosInstance1 = require("axios"); // mocked
_17 "Is axiosInstance1.get mocked:",
_17 jest.isMockFunction(axiosInstance1.get)
_17 jest.unmock("axios");
_17 const axiosInstance2 = require("axios"); // unmocked
_17 "Is axiosInstance2.get mocked:",
_17 jest.isMockFunction(axiosInstance2.get)
=== Output:Is axiosInstance1.get mocked: false
Is axiosInstance2.get mocked: false
Kapanış
İşin içine girip gerçek senaryolarla ilgili kod yazmadıkça test öğrenmek zor
görünebilir. Ancak olumsuz bir yargıya varmayın istediğimiz takdirde
öğrenebileceğimizi biliyoruz.
Yazı burada biter. Sağlıcakla kalın.
Kaynaklar