В этой статье я расскажу о насмешках 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

Тестирование кодов, использующих сторонние библиотеки, функциональность которых выходит за рамки тестирования. Некоторая общая библиотека, требующая насмешек в тестовой среде.

  1. Метод тестирования, который вызывает метод 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. ио»



И благодаря многочисленным ответам на переполнение стека.