Иногда вам хочется имитировать функцию в одних тестах, но не в других. Иногда вам нужно предоставить разные макеты для разных тестов. Jest усложняет задачу: его поведение по умолчанию заключается в переопределении функции пакета для всего тестового файла, а не только для одного теста. Это кажется странным, если вы использовали гибкие инструменты, такие как @patch Python или сервис-контейнер Laravel.
В этом посте показано, как имитировать функции для отдельных тестов, а затем возвращаться к исходной реализации, если макет не был предоставлен. Примеры будут приведены как для модулей CommonJS, так и для ES. Методы, продемонстрированные в этом посте, будут работать как для собственных модулей, так и для сторонних пакетов.
Поскольку в этом посте мы рассмотрим несколько модульных систем, важно понимать, что они из себя представляют.
CommonJS (сокращенно CJS) — это система модулей в Node.js. Он экспортирует функции с помощью модуля.exports и импортирует функции с помощью require():
// CommonJS export function greet() { return "Hello, world!"; } module.exports = { greet };
// CommonJS import const getUsersList = require('./greet');
Модули ES (сокращенно ESM) — это система модулей, используемая браузером. Он экспортирует функции, используя ключевое слово экспорта, и импортирует функции, используя ключевое слово импорта:
// ES module export export default function greet() { return "Hello, world!"; }
// ES module import import { greet } from "./greet";
На момент написания этой статьи большинство веб-разработчиков JavaScript используют ES-модули, и многие серверные JS-разработчики также используют их. Однако CommonJS по-прежнему используется по умолчанию для Node. Независимо от того, какую систему вы используете, стоит прочитать всю статью, чтобы узнать о системе насмешек Jest.
Обычно файл CommonJS экспортирует свои модули с использованием синтаксиса объекта, как показано ниже:
// CommonJS export function greet() { return "Hello, world!"; } module.exports = { greet: greet };
Однако можно экспортировать функцию и отдельно:
// CommonJS export function greet() { return "Hello, world!"; } module.exports = greet;
Я бы не рекомендовал делать это в вашем собственном коде: экспорт объекта доставит вам меньше головной боли при разработке приложения. Однако это настолько распространено, что стоит обсудить, как имитировать пустую экспортированную функцию в CommonJS, а затем вернуться к оригиналу, если тест не предоставляет собственную реализацию.
Предположим, у нас есть следующий файл CommonJS, который мы хотели бы имитировать во время тестов:
// cjsFunction.js function testFunc() { return "original"; } module.exports = testFunc;
Мы могли бы высмеять это в наших тестах, используя следующий код:
const testFunc = require("./cjsFunction"); jest.mock("./cjsFunction"); beforeEach(() => { testFunc.mockImplementation(jest.requireActual("./cjsFunction")); }); it("can override the implementation for a single test", () => { testFunc.mockImplementation(() => "mock implementation"); expect(testFunc()).toBe("mock implementation"); expect(testFunc.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { testFunc.mockReturnValue("mock return value"); expect(testFunc()).toBe("mock return value"); expect(testFunc.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(testFunc()).toBe("original"); expect(testFunc.mock.calls).toHaveLength(1); });
Когда мы вызываем jest.mock("./cjsFunction"), это заменяет модуль (файл и весь его экспорт) автоматическим макетом (документация). Когда вызывается автоматический макет, он возвращает неопределенное значение. Однако он предоставит методы для переопределения реализации макета, возвращаемого значения и многого другого. Вы можете увидеть все свойства и методы, которые он предоставляет, в документации Jest Mock Functions.
Мы можем использовать метод mockImplementation() макета, чтобы автоматически установить реализацию макета в соответствии с реализацией исходного модуля. Jest предоставляет метод jest.requireActual(), который всегда загружает исходный модуль, даже если в данный момент над ним издеваются.
Реализации макета и возвращаемые значения автоматически очищаются после каждого теста, поэтому мы можем передать функцию обратного вызова в функцию beforeEach() Jest, которая устанавливает реализацию макета в исходную реализацию перед каждым тестом. Тогда любые тесты, которые хотят предоставить собственное возвращаемое значение или реализацию, могут сделать это вручную в теле теста.
Предположим, что приведенный выше код экспортировал объект вместо одной функции:
// cjsModule.js function testFunc() { return "original"; } module.exports = { testFunc: testFunc, };
Тогда наши тесты будут выглядеть так:
const cjsModule = require("./cjsModule"); afterEach(() => { jest.restoreAllMocks(); }); it("can override the implementation for a single test", () => { jest .spyOn(cjsModule, "testFunc") .mockImplementation(() => "mock implementation"); expect(cjsModule.testFunc()).toBe("mock implementation"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { jest.spyOn(cjsModule, "testFunc").mockReturnValue("mock return value"); expect(cjsModule.testFunc()).toBe("mock return value"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(cjsModule.testFunc()).toBe("original"); }); it("can spy on calls while keeping the original implementation", () => { jest.spyOn(cjsModule, "testFunc"); expect(cjsModule.testFunc()).toBe("original"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); });
Метод jest.spyOn() позволяет Jest записывать вызовы метода объекта и предоставлять собственную замену. Это только работает с объектами, и мы можем его использовать, поскольку наш модуль экспортирует объект, содержащий нашу функцию.
Метод spyOn() является макетом, поэтому его состояние необходимо сбросить. В документации Jest spyOn() рекомендуется сбрасывать состояние с помощью jest.restoreAllMocks() в обратном вызове afterEach(), что мы и сделали выше. Если бы мы этого не сделали, макет вернул бы неопределенное значение в следующем тесте после вызова spyOn().
Модули ES могут иметь экспорт по умолчанию и именованный:
// esmModule.js export default function () { return "original default"; } export function named() { return "original named"; }
Вот как будут выглядеть тесты для файла выше:
import * as esmModule from "./esmModule"; afterEach(() => { jest.restoreAllMocks(); }); it("can override the implementation for a single test", () => { jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default"); jest .spyOn(esmModule, "named") .mockImplementation(() => "mock implementation named"); expect(esmModule.default()).toBe("mock implementation default"); expect(esmModule.named()).toBe("mock implementation named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { jest.spyOn(esmModule, "default").mockReturnValue("mock return value default"); jest.spyOn(esmModule, "named").mockReturnValue("mock return value named"); expect(esmModule.default()).toBe("mock return value default"); expect(esmModule.named()).toBe("mock return value named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(esmModule.default()).toBe("original default"); expect(esmModule.named()).toBe("original named"); });
Это выглядит почти так же, как и предыдущий пример CommonJS, с парой ключевых отличий.
Сначала мы импортируем наш модуль как импорт пространства имен.
import * as esmModule from "./esmModule";
Затем, когда мы хотим отслеживать экспорт по умолчанию, мы используем «default»:
jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default");
Иногда при попытке вызвать jest.spyOn() из стороннего пакета вы получаете сообщение об ошибке, подобное приведенному ниже:
TypeError: Cannot redefine property: useNavigate at Function.defineProperty ()
Когда вы столкнетесь с этой ошибкой, вам нужно будет имитировать пакет, вызывающий проблему:
import * as reactRouterDOM from "react-router-dom"; // ADD THIS: jest.mock("react-router-dom", () => { const originalModule = jest.requireActual("react-router-dom"); return { __esModule: true, ...originalModule, }; }); afterEach(() => { jest.restoreAllMocks(); });
Этот код заменяет модуль макетом модуля Jest ES, который содержит все исходные свойства модуля с использованием фабричного параметра jest.mocks. Свойство __esModule требуется всякий раз, когда используется фабричный параметр в jest.mock для имитации модуля ES (документация).
При желании вы также можете заменить отдельную функцию в заводском параметре. Например, React Router выдаст ошибку, если потребитель вызовет useNavigate() вне контекста Router, поэтому мы можем использовать jest.mock() для замены этой функции во всем тестовом файле, если захотим:
jest.mock("react-router-dom", () => { const originalModule = jest.requireActual("react-router-dom"); return { __esModule: true, ...originalModule, // Dummy that does nothing. useNavigate() { return function navigate(_location) { return; }; }, }; });
Надеюсь, эта информация будет полезна при написании собственных тестов. Не каждое приложение выиграет от возможности вернуться к реализации по умолчанию, если в самом тесте реализация не указана. Действительно, многие приложения захотят использовать один и тот же макет для всего файла тестирования. Однако методы, показанные в этом посте, дадут вам более детальный контроль над издевательством.
Дайте мне знать, если я что-то пропустил или есть что-то, что я не включил в этот пост, и это должно быть здесь.
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3