【问题标题】:TypeError during Jest's spyOn: Cannot set property getRequest of #<Object> which has only a getterJest spyOn 期间的 TypeError:无法设置只有一个 getter 的 #<Object> 的属性 getRequest
【发布时间】:2019-04-09 06:16:53
【问题描述】:

我正在用 TypeScript 编写一个 React 应用程序。我使用 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));
    })
  );

我想像这样使用 Jest 尝试 API 功能:

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 测试期间可能导致这种情况的原因是什么?我怎样才能让这个测试通过?

【问题讨论】:

  • 你需要在没有setter的es6模块对象上使用jest.mock()
  • @Volodymyr 你能解释一下你会怎么做吗?我真的不明白。我从来没有遇到过 getter 和 setter 的话题。在 React Native 上,这个测试也为我通过了。只有在常规 React 上才会失败。
  • 很奇怪。错误发生在 this line 上,其中 jest 尝试替换 serverRequests 模块对象上的属性 getRequest。我无法重现问题。 serverRequests 模块是否在其他地方被修改(可能是全局模拟的)?出于某种原因,getRequest 最终成为导入模块对象的 getter 属性,从而防止它被间谍替换。
  • @brian-lives-outdoors 在serverRequests/ 中有一个index.ts 可以导入所有请求并再次导出它们,这样我就可以执行import { getRequest } from "../../utils/serverRequests";。会不会是这个原因?

标签: reactjs unit-testing jestjs spy spyon


【解决方案1】:

如果您希望不修改导入,可以通过以下方式解决问题:

import * as lib from './lib'

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

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

您需要添加更多 jest.mock 行以用于您希望监视的任何其他重新导出的函数。

【讨论】:

    【解决方案2】:

    使用ts-jest作为编译器进行测试,如果您以这种方式模拟模块,它将起作用:

    import * as serverRequests from "./../../utils/serverRequests";
    
    jest.mock('./../../utils/serverRequests', () => ({
      __esModule: true,
      ...jest.requireActual('./../../utils/serverRequests')
    }));
    
    const getRequestMock = jest.spyOn(serverRequests, "getRequest");
    

    Jest oficial doc for __esModule

    【讨论】:

    • 这也是一个很好的答案。我尝试这样做是因为我必须监视无法直接导入子模块的 Azure 模块,也不能手动模拟整个对象,如 @brian-adams 所述。此代码将自动模拟导入的库。
    • +1 这可能是最好的答案,因为 ..jest.requireActual('./../../utils/serverRequests').
    【解决方案3】:

    最近我们在使用的库中遇到了类似的问题。 Babel 只为从库中导出的所有成员提供 getter,所以我们在测试的顶部这样做:

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

    这解决了这个问题,因为它创建了一个新的、普通的 JS 对象,其中包含库中每个属性的成员。

    【讨论】:

    • 除非您同时模拟任何实现,否则上述操作可以更简单地使用import * as myModuleRaw from 'myModule'; const myModule = { ...myModuleRaw};。如果您打算将jest.spyOn() 用于任何方法,则无论如何您都需要将模块作为对象。
    【解决方案4】:

    Jest 单元测试的升级失败,交叉时:

    export * from './serverRequests';
    

    直接引用文件以避免“...只有一个吸气剂”问题!

    【讨论】:

      【解决方案5】:

      对于遇到此问题的其他人,您可以将 babel 设置为使用“松散”转换,这为我解决了问题。只需将其设置在您的 .babelrc 文件中即可

      {
        "presets": [
          ["@babel/preset-env", {
            "loose": true
          }]
        ]
      }
      

      【讨论】:

        【解决方案6】:

        这个很有趣。

        问题

        Babel 生成的属性仅包含为重新导出的函数定义的 get

        utils/serverRequests/index.ts 从其他模块重新导出函数,因此当jest.spyOn 用于监视重新导出的函数时会引发错误。


        详情

        鉴于此代码从lib 重新导出所有内容:

        export * from './lib';
        

        ...Babel 产生这个:

        '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 尝试用包含原始函数的间谍替换该属性,但如果该属性仅使用@987654332 定义则不能@。


        解决方案

        不要将../../utils/serverRequests(重新导出getRequest)导入到测试中,而是导入定义getRequest的模块并使用该模块来创建间谍。

        替代解决方案

        按照@Volodymyr 和@TheF 的建议模拟整个utils/serverRequests 模块

        【讨论】:

        • 有趣的是,tscts-jest 为重新导出的函数生成正常属性,这就是为什么我最初很难重新创建问题。
        【解决方案7】:

        正如 cmets 中所建议的,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

        【讨论】:

        • 您分享的链接帮助我找到了 es6 类模拟的缺失部分!
        • 很好的答案谢谢,这是我总是必须重新开始的事情。还有几点。 1)我认为您不需要在这里监视getRequest 方法,因为您将其嘲笑为开玩笑的功能。您可以只 import { getRequest } from '.... 并将其传递给期望。 2)如果你想保留getRequest方法的原始实现,你可以做getRequest: jest.fn(jest.requireActual(...path).getRequest'
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-08-29
        • 2020-09-26
        • 2022-07-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-31
        相关资源
        最近更新 更多