Имитация зависимостей вашего кода - один из фундаментальных аспектов тестирования программного обеспечения, который позволяет разработчикам получить контроль над потоками данных и поведением кода.

В качестве среды тестирования JavaScript Jest имеет обширную коллекцию API, которые сделают нашу жизнь проще и помогут с имитацией зависимостей. Однако иногда вариантов бывает так много, что их сложно узнать все, не говоря уже о том, чтобы определить наиболее оптимальный.

У меня был аналогичный случай, когда я импортировал несколько разных экспортов из модуля @module/api и использовал его во всем своем коде. Однако в своих тестах я хотел имитировать одну конкретную импортированную функцию functionToMock и оставить все остальные импортированные нетронутыми.

Конкретная функция выполняла некоторую логику в фоновом режиме, которую нельзя было воспроизвести в тестовой среде, а также просто не имело значения для целостности моих тестов. Итак, я хотел высмеять это, но было важно, чтобы весь другой импорт по-прежнему работал так, как это будут воспринимать конечные пользователи.

Проведя некоторое исследование и опробовав различные подходы, я довольно много узнал о различных доступных имитационных подходах, различиях между ними и в целом лучшем понимании имитирующих зависимостей в Jest. В этой статье я поделюсь своими знаниями о том, как имитировать одну конкретную функцию из импортированного модуля в Jest.

Ручное издевательство

Главное, что я обнаружил после всего процесса, это то, что попытка имитировать одну конкретную функцию из импортированного модуля по сути то же самое, что имитировать любую функцию из любого другого модуля. Итак, имеет смысл начать с самого фундаментального подхода, а именно с имитации функции вручную.

import * as moduleApi from '@module/api';
// Somewhere in your test case or test suite
moduleApi.functionToMock = jest.fn().mockReturnValue({ someObjectProperty: 42 });

Здесь мы сначала импортируем весь импорт из @module/api, объединяем его в объект и сохраняем в переменной с именем moduleApi. Затем мы перезаписываем функцию, которую хотим имитировать functionToMock, с помощью фиктивной функции Jest. Это означает, что внутри нашей тестовой среды любые вызовы functionToMock из нашего кода будут запускать не фактическую функцию, а скорее эту шутливую фиктивную функцию.

После этого мы можем использовать служебные функции Jest, чтобы изменить поведение этой функции в зависимости от требований теста или набора тестов. В приведенном выше примере мы использовали mockReturnValue, чтобы фиктивная функция всегда возвращала определенное значение, которое в данном случае является объектом с определенным ключом и значением.

Это наиболее низкоуровневый подход, который должен работать в большинстве случаев использования. Другие подходы в основном используют служебные функции Jest, которые в той или иной форме являются абстракцией этого фундаментального подхода. Однако насмешка вручную довольно утомительна и требует ручного учета при работе с более сложными ситуациями. Следовательно, этот подход, вероятно, лучше всего использовать как запасной вариант после опробования встроенных служебных функций из Jest.

Также есть определенные случаи, когда этот подход не работает. При использовании этого подхода я чаще всего сталкивался с ошибкой TypeError: Cannot set property functionToMock of #<Object> which has only a getter. В этом случае вы можете попробовать один из других подходов, описанных в этой статье.

Слежка за функцией с помощью jest.spyOn

Другой способ имитировать конкретную функцию из импортированного модуля - использовать функцию jest.spyOn. API для этой функции кажется именно тем, что нам нужно для нашего варианта использования, поскольку он принимает весь модуль и конкретный экспорт, за которым следует следить.

import * as moduleApi from '@module/api';
// Somewhere in your test case or test suite
jest.spyOn(moduleApi, 'functionToMock').mockReturnValue({ someObjectProperty: 42 });

С точки зрения использования это в основном то же самое, что и имитация вручную, как описано в предыдущем разделе. Но это немного более чистый синтаксис, он позволяет упростить очистку фиктивных функций и упрощает выполнение утверждений для функции, поскольку jest.spyOn вернет фиктивную функцию. Но с точки зрения функциональности для этого варианта использования нет никакой разницы между слежением за функцией с помощью этого кода или ручным издевательством над ней.

Однако с технической точки зрения существует большая разница, потому что jest.spyOn(moduleApi, 'functionToMock') сам по себе будет запускать реальный functionToMock код, а не имитировать его. Слежка за функцией из модуля будет отслеживать только ее вызовы. Если вы также хотите издеваться над базовым кодом, вам придется связать его с обычными фиктивными служебными функциями, такими как mockReturnValue или mockImplementation.

При таком подходе есть вероятность, что вы наткнетесь на TypeError: Cannot redefine property: functionToMock at Function.defineProperty (<anonymous>). Это похоже на ошибку, с которой мы столкнулись при попытке ручного макета. Тем не менее, я бы посоветовал вам сначала попробовать выполнить ручное издевательство, чтобы решить проблему, если вы еще этого не сделали, поскольку накладные расходы не так велики. Но если и ручное издевательство, и слежка за функцией не работают, вы можете обратиться к следующему и последнему подходу.

Сделайте макет всего модуля и восстановите ненужные макеты с помощью jest.requireActual

В большинстве случаев один из других подходов должен помочь и удовлетворить ваш вариант использования. Но в редких случаях вы столкнетесь с ошибками, которые не позволят вам переопределить единственную экспортированную функцию. Это именно то, с чем я столкнулся, и решение, которое я использовал, выглядит следующим образом.

import { functionToMock } from "@module/api"; // Step 3.
// Step 1.
jest.mock("@module/api", () => {
    const original = jest.requireActual("@module/api"); // Step 2.
    return {
        ...original,
        functionToMock: jest.fn()
    };
});
// Step 4. Inside of your test suite:
functionToMock.mockImplementation(() => ({ mockedValue: 2 }));

Здесь много чего происходит, так что давайте разберемся с этим.

На шаге 1 мы используем jest.mock("@module/api", ...) для имитации всего модуля. Это означает, что каждый импорт из модуля будет фиктивной функцией в тестовой среде. Очевидно, это не то, что нам нужно, поскольку мы хотим только имитировать functionToMock экспорт. Мы можем решить это во втором аргументе вызова jest.mock, который принимает обратный вызов, который должен возвращать объект. Этот объект возвращается вместо фактического модуля, когда модуль каким-либо образом импортируется в нашу тестовую среду.

Затем на шаге 2 внутри обратного вызова второго аргумента мы используем jest.requireActual("@module/api") для захвата исходного кода, импорта из модуля и сохранения его в переменной. Затем мы создаем объект, который должен заменить импорт модуля, выполняя две вещи: помещаем в него весь исходный импорт и переопределяем functionToMock, который мы хотим имитировать, с помощью шутливой насмешливой функции.

Затем, чтобы использовать фиктивную функцию, мы должны импортировать функцию из модуля, шаг 3. Наконец, где-то внутри вашего набора тестов, шаг 4, вы можете использовать этот импорт для выполнения различных вещей, таких как настройка макетной реализации, как показано выше. пример кода или выполнение на нем утверждений.

По сути, мы смоделировали весь модуль, создали снимок фактического импорта модуля, использовали этот снимок в качестве макетной версии, а затем настроили любой импорт, как нам нравится, для нашей тестовой среды, переопределив его в макетном модуле. . В этом случае мы хотели только имитировать функцию functionToMock, поэтому нам нужно было только переопределить это с помощью фиктивной функции шутки.

Из-за того, что этот подход предполагает «выбросить все и начать с нуля», его лучше всего использовать в качестве последнего средства при попытке имитировать одну конкретную функцию из модуля в Jest.

Хотя этот подход будет работать во всех случаях, это довольно излишнее решение для того, чего мы пытаемся достичь, и может вызвать некоторую путаницу в будущем у других разработчиков. Если возможно, попробуйте использовать более изощренный подход - слежку за экспортом или даже вручную имитировать его. Но если ничего не помогает или два других подхода не работают, этот подход решит вашу проблему.

Больше контента на plainenglish.io