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?
Could you fill out the template, including a link to a runnable example showing the inconsistency?
I'll use the Bug Report template here.
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
clone this repo: https://github.com/Financial-Times/n-express-monitor
in src/__tests__/operation.js
add the module mock, manual mock and spyOn code as shown in the initial post
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 somewherehttps://github.com/Financial-Times/n-express-monitor
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:
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.:
defineProperty
resultexport let mutableValue
(it's fixable, but with more efforts)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);
});
});
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.