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

Created on 29 Aug 2018  路  16Comments  路  Source: facebook/jest

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?

Most helpful comment

@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.

All 16 comments

Could you fill out the template, including a link to a runnable example showing the inconsistency?

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

Any progress?

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

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

@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.

ModuleMockerClass._spyOnProperty throws that error if:

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

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

Hi @pmomot posted answer doesn't solve this bug. Still getting this bug after do
beforeEach(() => {
configure({ computedConfigurable: true });
});
in my test.

Hi @guerjon I've done it for all tests at once in setupTests file, maybe that's the difference?

@AnilGayakwad Thank you!

I came here after upgrading to TypeScript 3.9.2, and seeing some tests begin to fail because of some jest.spyOn calls not working anymore. Doing what you outlined fixed them!

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.

@faiwer what is the orDie in your unfreezeImport func above?

@BenBrewerBowman, sorry, I forgot to remove it in this sample of code. Actually it's just this:

import _ from 'lodash';

export const orDie = arg => {
  if (_.isNil(arg)) throw new Error(`Argument is empty`);
  return arg;
}

I use it to ensure in runtime that something that is supposed to exist exists. You can replace it with Object.getOwnPropertyDescriptor(module, key)! (! symbol)

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.

@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.

@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);
  });
});
Was this page helpful?
0 / 5 - 0 ratings