jest: jest.spyOn(module, 'method', 'get') failed as method not declared configurable

This seems to be an edge case around mocking or spyOn a module built with Babel, while it may be related to babel module export settings, but would like to get some insights here first to see if there’s anything I’ve missed or anything we can do on Jest side.

deps versions:

    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.3",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "jest": "^23.5.0",

Had a depedency dep-package built with babel with the following .babelrc:

{
  "presets": [
    [
      "env", { "targets": { "node": "6.1.0" } }
    ]
  ],
  "plugins": [
    "transform-object-rest-spread"
  ]
}

in test.js

import * as depPackage from 'dep-package';

jest.mock('dep-package');

module mocks would work

import * as depPackage from 'dep-package';

depPackage.compose = jest.fn();
import * as depPackage from 'dep-package';

jest.spyOn(depPackage, 'compose');

this would throw error TypeError: Cannot set property compose of [object Object] which has only a getter

import * as depPackage from 'dep-package';

jest.spyOn(depPackage, 'compose', 'get');

this would throw error compose is not declared configurable

Not sure what is causing the difference between module mock with jest.mock() and manual mock =jest.fn() here?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 24
  • Comments: 20 (2 by maintainers)

Most upvoted comments

@zhenyulin give a try with this,

jest.mock('dep-package', () => ({ ...jest.requireActual('dep-package'), })); const cwpDetectionAgency = require('dep-package');

instead of doing import * as depPackage from 'dep-package';

This has helped me, any thing which is out side of the current code base we have to do this.

Finally I got this. It’s an unrobust, but working solution, that monkey-patches Object.defineProperty in the setup file.

In setupTests.js:

const { defineProperty } = Object;
Object.defineProperty = function(object, name, meta) {
  if (meta.get && !meta.configurable) {
    // it might be an ES6 exports object
    return defineProperty(object, name, {
      ...meta,
      configurable: true, // prevent freezing
    });
  }

  return defineProperty(object, name, meta);
};

In tests helpers:

export const unfreezeImport = <T,>(module: T, key: keyof T): void => {
  const meta = orDie(Object.getOwnPropertyDescriptor(module, key));
  const getter = orDie(meta.get);

  const originalValue = getter() as T[typeof key];
  let currentValue = originalValue;
  let isMocked = false;

  Object.defineProperty(module, key, {
    ...meta,
    get: () => (isMocked ? currentValue : getter()),
    set(newValue: T[typeof key]) {
      isMocked = newValue !== originalValue;
      currentValue = newValue;
    },
  });
};

in tests:

import * as someModule from 'someModule';

unfreezeImport(someModule, 'someProperty');

it('someTest', () => {
  jest.spyOn(someModule, 'someProperty').mockImplementation(...);
  // test code
});

May be it will help someone. Also this unfreezeImport method can be united with the patched defineProperty in setupTests.js file.

I’ll use the Bug Report template here.

🐛 Bug Report

jest.mock('dep-package') works well, while manual mocking throw TypeError: Cannot set property compose of [object Object] which has only a getter, and spyOn ‘get’ throw error is not declared configurable

To Reproduce

Expected behavior

  • Manual mock should work if auto mock works
  • spyOn get shouldn’t throw an error, or if it indicates some limitation from how package is produced here, would be great to have some note in the docs somewhere

Link to repl or repo (highly encouraged)

https://github.com/Financial-Times/n-express-monitor

Run npx envinfo --preset jest

Paste the results here:


  System:
    OS: macOS High Sierra 10.13.6
    CPU: x64 Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz
  Binaries:
    Node: 8.10.0 - ~/.nvm/versions/node/v8.10.0/bin/node
    Yarn: 1.7.0 - /usr/local/bin/yarn
    npm: 6.3.0 - ~/.nvm/versions/node/v8.10.0/bin/npm
  npmPackages:
    jest: ^23.5.0 => 23.5.0

https://twitter.com/slicknet/status/782274190451671040

Feel free to send a PR if you want to see this fixed

if you have this error with jest and mobx this answer might help you https://github.com/mobxjs/mobx/issues/1867#issuecomment-518987737

@zhenyulin give a try with this,

jest.mock('dep-package', () => ({ ...jest.requireActual('dep-package'), })); const cwpDetectionAgency = require('dep-package');

instead of doing import * as depPackage from 'dep-package';

This has helped me, any thing which is out side of the current code base we have to do this.

Thanks @AnilGayakwad! that worked for me, a whole working example would look something like this:

// import * as serverLogger from '@my-server-logger';

jest.mock('@my-server-logger', () => ({
  ...jest.requireActual('@my-server-logger')
}));

const serverLogger = require('@my-server-logger');
const loggerSpy = jest.spyOn(serverLogger, 'logger');

describe('serverLogger', () => {
  it('should call the logger when ....', () => {
    ...
    .....
    expect(loggerSpy).toHaveBeenCalledTimes(1);
  });
});

@faiwer You have no idea how much this has helped. I have been trying to fix a testing helper w/ a spyon function that is used in ~300 tests. Bc it’s a helper function, I couldn’t replace it with mock like everyone has been suggesting. You saved my life lol.

ModuleMockerClass._spyOnProperty throws that error if:

  • call spyOn with accessType argment ‘get’ | ‘set’ AND,
  • configurable descriptor of the target propery is false

Also I started unfreezing all es6-like exports automatically. I intercept each defineProperty invocation, check that it looks like es6-export-like object, and make it configurable: true. After that jest.spyOn and any other similar mechanisms work.

But if you wanna do the same in your codebase you need to take into consideration that it’s a dirty hack that can potentially break something else. E.g.:

  • you might use some 3rd-party library that has checks for defineProperty result
  • you might use some 3rd-party library that leverages export let mutableValue (it’s fixable, but with more efforts)
  • it may become broken when typescript/babel/whatever will change its way of handling es6 import/exports

IMHO it still worths it. Because the only way I see to make it “properly” is to use real DI everywhere. Or just don’t write tests.