From c4e20ae208a32677ffb61c3a3a51ae6e04c79969 Mon Sep 17 00:00:00 2001 From: thomasabishop Date: Tue, 26 Sep 2023 20:25:41 +0100 Subject: [PATCH] js: examples of mocking with Jest --- .../JavaScript/Syntax/Testing_with_Jest.md | 284 +++++++++++++++++- 1 file changed, 282 insertions(+), 2 deletions(-) diff --git a/Programming_Languages/JavaScript/Syntax/Testing_with_Jest.md b/Programming_Languages/JavaScript/Syntax/Testing_with_Jest.md index daa5fd2..1869d5d 100644 --- a/Programming_Languages/JavaScript/Syntax/Testing_with_Jest.md +++ b/Programming_Languages/JavaScript/Syntax/Testing_with_Jest.md @@ -8,12 +8,292 @@ tags: [javascript, testing] ## Mock a function -## Mock a class/module +```js +function sumOfFirstTenNumbers() { + let sum = 0; + for (let i = 1; i <= 10; i++) { + sum += i; + } + return sum; +} +``` + +```js +test("mock sumOfFirstTenNumbers function", () => { + const mockFunction = jest.fn().mockReturnValue(100); + const result = mockFunction(); + expect(result).toBe(100); + expect(mockFunction).toHaveBeenCalled(); +}); +``` + +## Mocking classes/modules + +### Classes + +Let' say we have this class: + +```js +// database.js +class Database { + connect() {} + save(data) {} +} +``` + +Then to mock: + +```js +import Database from "./database"; + +// This will mock the whole Database class, replacing all methods with jest mock functions. +jest.mock("./database"); + +test("should use mocked save method", () => { + const dbInstance = new Database(); + + // Mocking the save method with a specific return value + dbInstance.save.mockReturnValue(true); + + const result = dbInstance.save({ key: "value" }); + + expect(result).toBe(true); + expect(dbInstance.save).toHaveBeenCalledWith({ key: "value" }); + + // The connect method is still a mock function (but without a specific behavior). + dbInstance.connect(); + expect(dbInstance.connect).toHaveBeenCalled(); +}); +``` + +### Modules + +Say we have the following module file: + +```js +// utils.js +export const doSomething = () => { + // ... +}; + +export const fetchUserData = async (userId) => { + const response = await axios.get(`/api/users/${userId}`); + return response.data; +}; +``` + +Mocked: + +```js +jest.mock("./utils", () => { + return { + doSomething: jest.fn(() => "mocked doSomething"), + fetchUserData: jest.fn((userId) => + Promise.resolve({ id: userId, name: "Mock User" }) + ), + }; +}); + +test("should use mocked module functions", () => { + expect(utils.doSomething()).toBe("mocked doSomething"); + expect(utils.doSomething).toHaveBeenCalled(); + + const result = await utils.fetchUserData(123); + + expect(result).toEqual({ id: 123, name: "Mock User" }); + expect(utils.fetchUserData).toHaveBeenCalledWith(123); + +}); +``` ## Check that a function has been called within another function -## Mock a function that needs to be called within another function +```js +function toBeCalledFunction() { + console.log("Original function called"); +} + +function callerFunction() { + toBeCalledFunction(); +} +``` + +```js +test("spy on toBeCalledFunction", () => { + const spy = jest.spyOn(global, "toBeCalledFunction"); // Replace `global` with the appropriate object/context if the function is not global + callerFunction(); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); // Restore the original function after spying +}); +``` + +## Mock a function that needs to resolve to something within another function + +We have two functions, one that gets data and another that processes it. We want to mock the function that gets data and return a value that the processing function can use. + +```js +async function getData() { + // ... Fetch some data from an API or database + return fetchedData; +} + +async function processData() { + const data = await getData(); + // ... Process the data + return processedData; +} +``` + +The mocking part: + +```js +const mockData = { key: "value" }; // Mocked data + +jest.mock("./path-to-file-where-getData-is", () => ({ + getData: jest.fn().mockResolvedValue(mockData), +})); + +test("test processData function", async () => { + const result = await processData(); + // Now, result contains the processed version of mockData + expect(result).toEqual(/* expected processed data based on mockData */); +}); +``` + +We could also combine the above with a spy to check that the `getData` function was called: + +```js +const getDataSpy = jest + .spyOn(moduleContainingGetData, "getData") + .mockResolvedValue(mockData); + +const result = await processData(); +expect(getDataSpy).toHaveBeenCalled(); +expect(result).toEqual(/* expected processed data based on mockData */); +getDataSpy.mockRestore(); +``` ## Mock a function that takes arguments +```js +function addPrefix(str) { + return `prefix-${str}`; +} +``` + +```js +test("dynamic mock for addPrefix function", () => { + const mockFunction = jest.fn((str) => `mock-${str}`); + + // Example usage of mockFunction + const result1 = mockFunction("test"); + const result2 = mockFunction("example"); + + expect(result1).toBe("mock-test"); + expect(result2).toBe("mock-example"); +}); +``` + ## Mocking network requests + +### Mocking Axios + +```js +jest.mock("axios", () => ({ + get: jest.fn().mockResolvedValue(mockData), + post: jest.fn().mockResolvedValue(mockData), +})); +``` + +Or we could implement this way: + +```js +jest.mock("axios"); +axios.get.mockResolvedValue({ data: "mockedData" }); +axios.post.mockResolvedValue({ data: "mockedData" }); +``` + +Then we can use the mocked axios functions in our tests: + +```js +const result = await fetchData(); // the function that uses Axios `get`` +expect(result).toBe("mockedGetData"); + +const result = await sendData({ key: "value" }); // the function tha uses Axios `post` +expect(result).toBe("mockedPostData"); +``` + +### `mockImplementation` + +For more configurable cases we can use `mockImplementation`: + +```js +it("sends data", async () => { + // Mock axios.post using mockImplementation + axios.post.mockImplementation((url, data) => { + if (data.key === "value") { + return Promise.resolve({ data: "mockedPostData" }); + } else { + return Promise.reject({ error: "An error occurred" }); + } + }); + + const result = await sendData({ key: "value" }); + + expect(result).toBe("mockedPostData"); +}); +``` + +If we want to change the `get` and `post` values in different tests, we can do so by using `mockImplementation`: + +## Mocking exceptions + +Again we use `mockImplementation`: + +Say we have the following function: + +```js +// fetchData.js +import axios from "axios"; + +const fetchData = async (url) => { + try { + const response = await axios.get(url); + return response.data; + } catch (error) { + throw new Error("Error fetching data"); + } +}; + +export default fetchData; +``` + +We would mock the success and the error as follows: + +```js +import axios from "axios"; +import fetchData from "./fetchData"; + +jest.mock("axios"); + +describe("fetchData", () => { + it("fetches data successfully", async () => { + axios.get.mockResolvedValue({ data: "mockedData" }); + + const result = await fetchData("https://api.example.com/data"); + + expect(result).toBe("mockedData"); + }); + + it("throws an error when fetching fails", async () => { + axios.get.mockImplementation(() => { + throw new Error("API error"); + }); + + // We use an asynchronous assertion here because we're expecting a promise to reject + await expect(fetchData("https://api.example.com/data")).rejects.toThrow( + "Error fetching data" + ); + }); +}); +```