TypeError during Jest's spyOn: Cannot set property getRequest of #<Object> which has only a getter

ReactjsUnit TestingJestjsSpySpyon

Reactjs Problem Overview


I'm writing a React application with TypeScript. I do my unit tests using Jest.

I have a function that makes an API call:

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);

The getRequest function looks like this:

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));
    })
  );

I wanted to try the API function using Jest like this:

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);
    });
  });
});

The problem is that the line with spyOn throws the following error:

  ● 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)

I googled this and only found posts about hot reloading. So what could cause this during Jest test? How can I get this test to pass?

Reactjs Solutions


Solution 1 - Reactjs

This one was interesting.

Issue

Babel generates properties with only get defined for re-exported functions.

utils/serverRequests/index.ts re-exports functions from other modules so an error is thrown when jest.spyOn is used to spy on the re-exported functions.


Details

Given this code re-exporting everything from lib:

export * from './lib';

...Babel produces this:

'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];
    }
  });
});

Note that the properties are all defined with only get.

Trying to use jest.spyOn on any of those properties will generate the error you are seeing because jest.spyOn tries to replace the property with a spy wrapping the original function but can't if the property is defined with only get.


Solution

Instead of importing ../../utils/serverRequests (which re-exports getRequest) into the test, import the module where getRequest is defined and use that module to create the spy.

Alternate Solution

Mock the entire utils/serverRequests module as suggested by @Volodymyr and @TheF

Solution 2 - Reactjs

As suggested in the comments, jest requires a setter on the tested object which es6 module objects don't have. jest.mock() allows you solving this by mocking your required module after the import.

Try mocking the exports from your serverRequests file

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);
    });
});

Here are some useful links:
https://jestjs.io/docs/en/es6-class-mocks<br> https://jestjs.io/docs/en/mock-functions

Solution 3 - Reactjs

Tested with ts-jest as compiler, it will work if you mock the module in this way:

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

Solution 4 - Reactjs

Recently we hit something like this in library we were using. Babel was only providing getters for all the members that were exported from the library, so we did this at the top of the test:

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

This fixed the issue, as it created a new, plain-old JS object with a member for every property in the library.

Solution 5 - Reactjs

For anyone else having this issue you can set babel to use "loose" transformations which solved the problem for me. Just set it in your .babelrc file as so

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

Solution 6 - Reactjs

If you wish to leave your import unmodified, you can fix the issue this way:

import * as lib from './lib'

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

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

You'll need to add more jest.mock lines for any additional re-exported functions you wish to spy on.

Solution 7 - Reactjs

Upgrades on Jest Unit-Tests fail, when crossing:

export * from './serverRequests';

Reference the files directly to avoid "...has only a getter" issues!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJ. HestersView Question on Stackoverflow
Solution 1 - ReactjsBrian AdamsView Answer on Stackoverflow
Solution 2 - ReactjsThe FView Answer on Stackoverflow
Solution 3 - ReactjsiarroyoView Answer on Stackoverflow
Solution 4 - ReactjstvsbrentView Answer on Stackoverflow
Solution 5 - ReactjsGary ForsterView Answer on Stackoverflow
Solution 6 - ReactjsNathan ArthurView Answer on Stackoverflow
Solution 7 - ReactjsJason MullingsView Answer on Stackoverflow