Ошибка TypeError во время spyOn Jest: невозможно установить свойство getRequest для # ‹Object›, у которого есть только геттер.

Я пишу приложение React с TypeScript. Я провожу свои модульные тесты с помощью Jest.

У меня есть функция, которая выполняет вызов API:

import { ROUTE_INT_QUESTIONS } from "../../../config/constants/routes";
import { intQuestionSchema } from "../../../config/schemas/intQuestions";
import { getRequest } from "../../utils/serverRequests";

const intQuestionListSchema = [intQuestionSchema];

export const getIntQuestionList = () => getRequest(ROUTE_INT_QUESTIONS, intQuestionListSchema);

Функция getRequest выглядит так:

import { Schema } from "normalizr";
import { camelizeAndNormalize } from "../../core";

export const getRequest = (fullUrlRoute: string, schema: Schema) =>
  fetch(fullUrlRoute).then(response =>
    response.json().then(json => {
      if (!response.ok) {
        return Promise.reject(json);
      }
      return Promise.resolve(camelizeAndNormalize(json, schema));
    })
  );

Я хотел попробовать функцию API с помощью Jest вот так:

import fetch from "jest-fetch-mock";
import { ROUTE_INT_QUESTIONS } from "../../../config/constants/routes";
import {
  normalizedIntQuestionListResponse as expected,
  rawIntQuestionListResponse as response
} from "../../../config/fixtures";
import { intQuestionSchema } from "../../../config/schemas/intQuestions";
import * as serverRequests from "./../../utils/serverRequests";
import { getIntQuestionList } from "./intQuestions";

const intQuestionListSchema = [intQuestionSchema];

describe("getIntQuestionList", () => {
  beforeEach(() => {
    fetch.resetMocks();
  });

  it("should get the int question list", () => {
    const getRequestMock = jest.spyOn(serverRequests, "getRequest");
    fetch.mockResponseOnce(JSON.stringify(response));

    expect.assertions(2);
    return getIntQuestionList().then(res => {
      expect(res).toEqual(expected);
      expect(getRequestMock).toHaveBeenCalledWith(ROUTE_INT_QUESTIONS, intQuestionListSchema);
    });
  });
});

Проблема в том, что строка с spyOn выдает следующую ошибку:

  ● getRestaurantList › should get the restaurant list

    TypeError: Cannot set property getRequest of #<Object> which has only a getter

      17 |
      18 |   it("should get the restaurant list", () => {
    > 19 |     const getRequestMock = jest.spyOn(serverRequests, "getRequest");
         |                                 ^
      20 |     fetch.mockResponseOnce(JSON.stringify(response));
      21 |
      22 |     expect.assertions(2);

      at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:706:26)
      at Object.spyOn (src/services/api/IntQuestions/intQuestions.test.ts:19:33)

Я погуглил и нашел только сообщения о горячей перезагрузке. Так что могло вызвать это во время теста Jest? Как я могу пройти этот тест?


person J. Hesters    schedule 05.11.2018    source источник
comment
Вам нужно использовать jest.mock() на объектах модуля es6, у которых нет установщиков   -  person Volodymyr    schedule 06.11.2018
comment
@Volodymyr Не могли бы вы объяснить, как бы вы это сделали? Я не совсем понимаю. Я никогда не сталкивался с темой геттеров и сеттеров. Также этот тест проходит у меня на React Native. Только на обычном React он не работает.   -  person J. Hesters    schedule 06.11.2018
comment
@ brian-live-outdoors В serverRequests/ есть index.ts, который импортирует все запросы и снова экспортирует их, так что я могу делать import { getRequest } from "../../utils/serverRequests";. Не в этом ли причина?   -  person J. Hesters    schedule 14.11.2018


Ответы (7)


Это было интересно.

Проблема

Babel генерирует свойства, в которых только get определены для реэкспортируемых функций.

utils/serverRequests/index.ts реэкспортирует функции из других модулей, поэтому возникает ошибка, когда jest.spyOn используется для слежения за реэкспортируемыми функциями.


Подробности

Учитывая этот код, реэкспорт всего из lib:

export * from './lib';

..._ 7_ производит это:

'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _lib = require('./lib');

Object.keys(_lib).forEach(function (key) {
  if (key === "default" || key === "__esModule") return;
  Object.defineProperty(exports, key, {
    enumerable: true,
    get: function get() {
      return _lib[key];
    }
  });
});

Обратите внимание, что все свойства определены только с get.

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


Решение

Вместо импорта ../../utils/serverRequests (который повторно экспортирует getRequest) в тест, импортируйте модуль, в котором определено getRequest, и используйте этот модуль для создания шпиона.

Альтернативное решение

Смоделируйте весь модуль utils/serverRequests, как это предлагают @Volodymyr и @TheF

person Brian Adams    schedule 14.11.2018
comment
Интересно, что tsc и ts-jest генерируют нормальные свойства для повторно экспортированных функций, поэтому мне было трудно сначала воссоздать проблему. - person Brian Adams; 14.11.2018

Как предлагается в комментариях, для jest требуется установщик для тестируемого объекта, которого нет у объектов модуля es6. jest.mock() позволяет решить эту проблему путем имитации необходимого модуля после импорта.

Попробуйте издеваться над экспортом из файла serverRequests

import * as serverRequests from './../../utils/serverRequests';
jest.mock('./../../utils/serverRequests', () => ({
    getRequest: jest.fn()
}));

// ...
// ...

it("should get the int question list", () => {
    const getRequestMock = jest.spyOn(serverRequests, "getRequest")
    fetch.mockResponseOnce(JSON.stringify(response));

    expect.assertions(2);
    return getIntQuestionList().then(res => {
        expect(res).toEqual(expected);
          expect(getRequestMock).toHaveBeenCalledWith(ROUTE_INT_QUESTIONS, intQuestionListSchema);
    });
});

Вот несколько полезных ссылок:
https://jestjs.io/docs/en/es6-class-mocks
https://jestjs.io/docs/en/mock-functions

person The F    schedule 08.11.2018
comment
ссылки, которыми вы поделились, помогли мне найти недостающие части макетов класса es6! - person piouson; 03.02.2021
comment
Хороший ответ, спасибо, это то, к чему я всегда должен возвращаться. Пара дополнительных моментов. 1) Я не думаю, что вам нужно шпионить за методом getRequest, поскольку вы высмеивали его как функцию шутки. Вы можете просто import { getRequest } from '.... и передать это ожиданию. 2) Если вы хотите сохранить исходную реализацию метода getRequest, вы можете сделать getRequest: jest.fn(jest.requireActual(...path).getRequest' - person neviovalsa; 05.03.2021

Протестировано с ts-jest в качестве компилятора, оно будет работать, если вы имитируете модуль следующим образом:

import * as serverRequests from "./../../utils/serverRequests";

jest.mock('./../../utils/serverRequests', () => ({
  __esModule: true,
  ...jest.requireActual('./../../utils/serverRequests')
}));

const getRequestMock = jest.spyOn(serverRequests, "getRequest");

Официальный документ для __esModule

person iarroyo    schedule 12.08.2020
comment
Это тоже хороший ответ. Я попробовал это, потому что мне пришлось следить за модулем Azure, из которого я не мог напрямую импортировать подмодуль, а также вручную имитировать весь объект, как указано @ brian-adams. Этот код автоматически имитирует импортированную библиотеку. - person Michaelsoft; 12.06.2021

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

jest.mock('some-library', () => ({
  ...jest.requireActual('some-library')
}));

Это устранило проблему, поскольку он создал новый простой старый объект JS с членом для каждого свойства в библиотеке.

person tvsbrent    schedule 03.08.2020
comment
Если вы одновременно не издеваетесь над какой-либо реализацией, то описанное выше можно сделать проще, используя import * as myModuleRaw from 'myModule'; const myModule = { ...myModuleRaw};. Если вы собираетесь использовать jest.spyOn() для любого из методов, вам в любом случае понадобится модуль как объект. - person Katie Byers; 25.08.2020

Для всех, у кого есть эта проблема, вы можете настроить babel на использование "свободных" преобразований, которые решили проблему для меня. Просто установите его в своем файле .babelrc так

{
  "presets": [
    ["@babel/preset-env", {
      "loose": true
    }]
  ]
}
person Gary Forster    schedule 15.05.2020

Апгрейды на Jest Unit-Tests терпят неудачу при пересечении:

export * from './serverRequests';

Ссылайтесь на файлы напрямую, чтобы избежать проблем типа "... имеет только геттер"!

person Jason Mullings    schedule 20.06.2020

Если вы хотите оставить свой импорт без изменений, вы можете решить проблему следующим образом:

import * as lib from './lib'

jest.mock('./lib/subModule')

it('can be mocked', () => {
  jest.spyOn(lib, 'subModuleFunction')
})

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

person Nathan Arthur    schedule 09.12.2020