В этой статье я расскажу о насмешках 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. ио»
И благодаря многочисленным ответам на переполнение стека.