В этой статье я расскажу о насмешках Jest, различных случаях использования, когда насмешки становятся сложными, а тестирование — пугающим. Цель этой статьи — привести пример для наиболее распространенных и не очень распространенных сценариев тестирования.
Итак, давайте погрузимся в статью
Что такое насмешка и зачем нам насмешка?
Короче говоря, насмешка над любой функцией, объектом или классом создает фиктивную дублирующую реализацию, чтобы мы могли избежать вызова фактической зависимости, которая выходит за рамки тестирования.
Тестовый сценарий 1
Чтобы протестировать первый класс, мы имитируем второй класс, от которого зависит первый класс.
Здесь мы хотим убедиться, что функция `area` в модуле `Circle` работает правильно, независимо от того, что возвращает функция `multiply`.
import {multiply} from './maths' export function area(radius:number) { return multiply(Math.PI, multiply(radius, radius)) }
Итак, в нашем тестовом примере мы будем издеваться над несколькими функциями.
import {area} from './circle' jest.mock('./maths', () => ({ multiply: jest.fn().mockReturnValue(1) })); describe('area', () => { test("calculates the area of a circle", () => { expect(area(1)).toBe(1) }) });
Тестовый сценарий 2:
Что, если мы не хотим имитировать всю функцию внутри «математического» модуля и хотим, чтобы имитировалось только умножение, мы могли бы использовать jest.requireActual, чтобы сохранить всю исходную реализацию, кроме умножения.
import {area} from './circle' import {multiply} from './maths' jest.mock('./maths', () => ({ ...jest.requireActual('./maths'), multiply: jest.fn().mockReturnValue(1) })); describe('area', () => { test("calculates the area of a circle", () => { expect(area(1)).toBe(1) expect(multiply).toHaveBeenCalled(); }) });
Тест Сенарио 3:
Имитационные переменные, такие как переменные среды и глобальные переменные, иногда необходимы для настройки теста. Одним из интересных вариантов использования является имитация переменной среды NODE_ENV для проверки поведения кода в разных средах:
export function config(){ if(process.env.NODE_ENV === "production"){ return "production" }else if(process.env.NODE_ENV === "development"){ return "development" }else{ return "test" } }
Для тестирования мы могли бы использовать следующий подход, здесь мы не использовали макет из шутки, а вместо этого изменили реальные значения.
describe('config', () => { const env = {...process.env} ; // save current env afterAll(() => { process.env = env; }); it("returns production when NODE_ENV is production", () => { process.env.NODE_ENV = "production"; expect(config()).toBe("production") }); it("returns development when NODE_ENV is development", () => { process.env.NODE_ENV = "development"; expect(config()).toBe("development") }); });
Здесь обсуждается лучший и более подробный подход:
Как имитировать process.env при написании модульных тестов с помощью Jest?
Тестовый сценарий 4
При работе с цепочками функций, такими как методы, которые вызываются в цепочке, например `response.status().send()`, нам нужно имитировать каждый связанный метод:
const mockResponse = () => { const res = {} as unknown as Response; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); res.send = jest.fn().mockReturnValue(res); return res; }; await testMyFunction(mockRequest, mockResponse, mockNext); expect(mockResponse().status).toHaveBeenCalledWith(200);
Тестовый сценарий 5
Тестирование асинхронного метода, который вызывается внутри другого метода
Если вызывающий метод не является асинхронным. В этом случае он не может ожидать другого асинхронного метода, например:
async function getSomeInformation( x: any) { let n: ReturnType<typeof setTimeout>; n = setTimeout(() =>{console.log("Hello From Async function")}, 1000); clearTimeout(n); } export function testA(req:any,res:any,next:any){ if(req?.header?.("test") == "true"){ console.log("Inside If"); next() } // couln't await for getSomeInformation to complete getSomeInformation("test").then(() => { console.log("Inside Then"); next(); }).catch(() => { console.log("Inside Catch"); next(); }); }
теперь, когда мы тестируем метод «testA», мы не можем проверить, потому что вызывается наша следующая операция, но jest не может подтвердить это перед закрытием.
import {testA} from './examples' describe('testA', () => { it("testA when ", async () => { const req = { header: jest.fn().mockReturnValue("true") } const res = {} const next = jest.fn(()=>{console.log("Inside Next")}) await testA(req,res,next) //following will fail, Although our next is called twice, // but jest environment is torn before compliler can return from getSomeInformation expect(next).toHaveBeenCalledTimes(2); }) });
Чтобы исправить это, мы будем ждать getSomeInformation внутри метода testA, что сделает его также асинхронным.
async function getSomeInformation( x: any) { let n: ReturnType<typeof setTimeout>; n = setTimeout(() =>{console.log("Hello From Async function")}, 1000); clearTimeout(n); } export async function testA(req:any,res:any,next:any){ if(req?.header?.("test") == "true"){ console.log("Inside If"); next() } await getSomeInformation("test").then(() => { console.log("Inside Then"); next(); }).catch(() => { console.log("Inside Catch"); next(); }); }
Тестовый сценарий 6
Тестирование кодов, использующих сторонние библиотеки, функциональность которых выходит за рамки тестирования. Некоторая общая библиотека, требующая насмешек в тестовой среде.
- Метод тестирования, который вызывает метод axios, при тестировании мы не хотим выполнять фактический вызов API.
import axios from "axios"; export async function getUserDetails() { const response = await axios.get("https://example.com/users", { headers: { "Content-Type": "application/json", }, }) return response.data; }
Чтобы протестировать этот вышеприведенный метод, мы можем смоделировать axios, а затем проверить, был ли вызван наш метод получения axios и какой аргумент был передан. Мы также имитируем ответ, чтобы мы могли продолжить тестирование функций пользователей, не делая вызовов.
import axios from "axios"; import { getUserDetails } from "./users"; jest.mock("axios"); describe("mocking axios", () => { it("should fetch users", async () => { const users = [{name: "Bob"}]; const resp = {data: users}; (axios.get as jest.Mock).mockResolvedValue(resp); // asume getUserDetails which calls axios.get const actualOutput = await getUserDetails(); expect(axios.get).toHaveBeenCalledTimes(1); expect(axios.get).toHaveBeenCalledWith("https://example.com/users", { headers: { "Content-Type": "application/json", }, }); }); });
Точно так же мы могли бы протестировать другие методы REST.
2. Методы тестирования, зависящие от проверки jwt, например:
import jwt from "jsonwebtoken"; const mySecretKey= "adminuserKey" export async function testMySecurity(req,res,next: any) { const token = req?.header?.authorization?.split(" ")[1]; jwt.verify(token, mySecretKey, (err, authData) => { if (err) { console.log("Error in verifying token"); res.status(403).send("Forbidden"); } else { next(authData); } }); }
Чтобы проверить это, мы не можем фактически предоставить токен JWT, подписанный закрытым ключом для тестирования, вместо этого мы будем следить за методом проверки и обходить фактический процесс проверки и вместо этого проверять, что будет делать наша функция, если проверка не пройдена или пройдена в режиме реального времени.
import jwt from "jsonwebtoken"; import { testMySecurity } from "./security"; import { Request,Response } from "express"; const mockResponse = () => { const res = {} as unknown as Response; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); res.send = jest.fn().mockReturnValue(res); return res; }; describe("testMySecurity", () => { it("should call next by bypassing verification", async () => { jest.spyOn(jwt, "verify") .mockImplementation(jest.fn((token, secretOrPublicKey, callback) => callback(null, { sub: "user_id" }))); const mockRequest = { header: { authorization: "BearerToken testToken", } as unknown as Request, }; const mockNext = jest.fn(() => "next"); await testMySecurity(mockRequest, mockResponse, mockNext); expect(mockNext).toHaveBeenCalledTimes(1); }); it("should call next by bypassing verification", async () => { jest.spyOn(jwt, "verify") .mockImplementation(jest.fn((token, secretOrPublicKey, callback) => callback( new Error("Forbidden"), null))); const mockRequest = { header: { authorization: "", } as unknown as Request, }; const mockNext = jest.fn(() => "next"); await testMySecurity(mockRequest, mockResponse, mockNext); expect(mockResponse().status).toHaveBeenCalledWith(403); }); });
3. Любая другая библиотека, например, предполагает, что admin является библиотекой, и нам нужно имитировать метод аутентификации внутри нее, мы можем имитировать, шпионя за методом, который мы намерены обойти, таким образом, шпионаж помогает нам имитировать модули внутри этого оператора, и мы можем изменить ожидание каждый раз по единичным случаям
import adminMock from "admin"; const mockAuth = {} as unknown as adminMock.auth.Auth; describe(" testAuthorisation",() =>{ it("test authorisation withod using admin library", async() =>{ mockAuth.verifyIdToken = jest.fn(mockRejectedIdToken); //spy on auth method of admin, and mock the impelementation to mockAuth jest.spyOn(adminMock, "auth").mockImplementation(() => mockAuth); }); });
Тестовый сценарий 7
При тестировании асинхронного метода во многих случаях один метод может зависеть от другого асинхронного метода, и для проверки функциональности первого метода нам потребуется имитировать вторую функцию. Например, здесь getUserDetails зависит от getUserBalance.
import axios from "axios"; export async function getUserDetails() { const userBalance: number= await getUserBalance("123"); if(userBalance >= 100) { return "rich"; }else { return "poor"; } } async function getUserBalance(id: string) { const userBalance:number = await new Promise(() => { return axios.get(`https://example.com/users/${id}/balance`, { headers: { "Content-Type": "application/json", }, }); }) return userBalance; }
Для этого нам нужно создать отдельные файлы для каждого такого метода, который необходимо имитировать. Следовательно, мы создадим еще один файл, который будет иметь
import axios from "axios"; export default async function getUserBalance(id: string) { const userBalance:number = await new Promise(() => { return axios.get(`https://example.com/users/${id}/balance`, { headers: { "Content-Type": "application/json", }, }); }) return userBalance; }
И чтобы смоделировать это, мы создадим отдельные имена вложенных папок __mocks__, и создадим файл с тем же именем, который возвращает фиктивные функциидляgetUserBalance, например
const getUserBalance = () => new Promise((resolve) => { resolve( 200 ); }); export default getUserBalance;
Теперь мы можем создать наш тестовый пример, как показано ниже.
import { getUserDetails } from "./index"; jest.mock("./getUserBalance",()=>{ return jest.fn().mockReturnValueOnce(200).mockReturnValueOnce(99); }); describe("test async method", () => { it("should return rich", async () => { const userBalance = 200; const resp = {data: userBalance}; const actualOutput = await getUserDetails(); expect(actualOutput).toEqual("rich"); }); it("should return poor", async () => { const actualOutput = await getUserDetails(); expect(actualOutput).toEqual("poor"); }); });
Таким образом, мы можем тестировать наши зависимые методы с помощью макетов.
Примечание:
jest.mock будет выполнен первым и будет имитировать все вхождения модулей.
В заключение, использование моков Jest имеет решающее значение для эффективного модульного тестирования. Понимая и применяя эти методы, вы можете обеспечить надежное и всестороннее тестирование своей кодовой базы. Помните, что `jest.mock` выполняется первым и имитирует все вхождения указанных модулей. Это позволяет адаптировать тесты к различным сценариям и сосредоточиться на поведении, которое вы собираетесь проверить.
Весь код можно найти здесь
Использованная литература:
Имитационные функции · Jest
Имитационные функции также известны как «шпионы, потому что они позволяют вам следить за поведением функции, которая называется…jestjs. ио»
И благодаря многочисленным ответам на переполнение стека.