Reputation: 31
service.js
this file defines a Service class with a constructor that initializes a value property and a getValue method that returns this value. The file exports an instance of the Service class.
class Service {
constructor() {
this.value = 'A';
}
getValue() {
return this.value;
}
}
export default new Service();
main.js
this file imports a service and defines a run function that logs a message with a value obtained from the service.
import service from './service';
export function run() {
const value = service.getValue();
console.log('Executing service with value ' + value);
}
Main.test.js
import { jest } from '@jest/globals';
import { run } from './main';
jest.mock('./service', () => {
return jest.fn().mockImplementation(() => {
return {
getValue: jest.fn().mockReturnValue('mocked_value'),
};
});
});
describe('test run', () => {
it('should log the correct message', () => {
console.log = jest.fn();
run();
expect(console.log).toHaveBeenCalledWith('Executing service with value mocked_value');
});
});
What happens?
Expected: "Executing service with value mocked_value"
Received: "Executing service with value A"
Can anyone help getting the mock work? Thanks.
Upvotes: 0
Views: 92
Reputation: 31
The issue is due to the fact that the mock for ./service
is not affecting the service
instance that was imported into the main.js
file. In Jest, jest.mock()
works by replacing the module during the import phase, but since the service
module is imported and instantiated as a singleton (new Service()
), it's not properly mocked in my initial try.
Here's a breakdown of the issue:
service.js
, exporting a singleton instance of the Service
class:
export default new Service();
main.js
, the service
import is directly using that singleton:
import service from './service';
Main.test.js
, trying to mock getValue()
using jest.mock()
, but because service.js
is already instantiated before the test runs, the mock does not override the singleton instance properly.https://github.com/aexel90/jest_mock/tree/babel
npm install --save-dev babel-jest
npm install @babel/preset-env --save-dev
babel.config.json
{
"presets": [
"@babel/preset-env"
]
}
package.json
{
...
"scripts": {
"test": "node --experimental-require-module node_modules/jest/bin/jest.js"
},
...
}
import { jest } from '@jest/globals';
import { run } from './main';
jest.mock('./service', () => {
return {
__esModule: true,
default: {
getValue: jest.fn().mockReturnValue('mocked_value'),
},
};
});
describe('test run', () => {
it('should log the correct message', () => {
console.log = jest.fn();
run();
expect(console.log).toHaveBeenCalledWith('Executing service with value mocked_value');
});
});
https://github.com/aexel90/jest_mock
import { jest } from '@jest/globals';
jest.unstable_mockModule('./service', async () => {
class MockedService {
getValue() {
return 'mocked_value';
}
}
return {
default: new MockedService(),
};
});
describe('test run', () => {
it('should log the correct message', async () => {
const { run } = await import('./main');
console.log = jest.fn();
run();
expect(console.log).toHaveBeenCalledWith('Executing service with value mocked_value');
});
});
The unstable_mockModule
function is called with the path to the module to be mocked and an asynchronous function that returns the mocked implementation.
In this case, the mocked implementation is a class MockedService
with a getValue
method that returns the string mocked_value
.
With await import(...)
the instantiation happens after the mock is already active.
An instance of this mocked class is then returned as the default export of the ./service
module => the singleton could be mocked
import { jest } from '@jest/globals';
jest.unstable_mockModule('./service', async () => {
class MockedService {
getValue() {
return 'default_value';
}
}
return {
default: new MockedService(),
};
});
const { run } = await import('./main');
const { default: service } = await import('./service');
describe('test run', () => {
it('should log the correct message with default_value', async () => {
console.log = jest.fn();
run();
expect(console.log).toHaveBeenCalledWith('Executing service with value default_value');
});
it('should log the correct message with mocked_value', async () => {
service.constructor.prototype.getValue = function (getValue) {
return 'mocked_value';
};
console.log = jest.fn();
run();
expect(console.log).toHaveBeenCalledWith('Executing service with value mocked_value');
});
});
Upvotes: 0
Reputation: 61
Did you consider the possibility to switch to Vitest? It provides a Jest compatible API and works as a drop-in replacement.
I know this doesn't answer your question directly, but it might be a good alternative to consider.
Your test case written for Vitest would look like this:
import { vi, describe, it, expect } from 'vitest';
import { run } from './main';
import Service from './service';
describe('test run', () => {
it('should log the correct message', () => {
const mock = vi.spyOn(Service, 'getValue');
mock.mockReturnValue('mocked_value');
console.log = vi.fn();
run();
expect(console.log).toHaveBeenCalledWith('Executing service with value mocked_value');
});
});
For this no additional configuration for Vitest is required.
In Jest I don't see real progress on supporting ESM out of the box during a really long time. And as you can see in the comments it requires quite some configuration to bring it to work with Jest.
I also had a quick try setting it up in Jest, but I didn't get it to work quickly. It's possible in Jest, but it's not as easy as it should be.
At the time of writing I think Jest feels to me like a dead end.
Upvotes: 0
Reputation: 222750
The problem is that Jest project isn't configured correctly to mock modules, currently it uses native ES modules, and jest.mock
does nothing. Module mocking is limited with native ESM and only works with dynamic imports and jest.unstable_mockModule
.
The current edition of the question contains incorrect mock, the previously used one should be used:
jest.mock('./service', () => {
return {
__esModule: true,
default: {
getValue: jest.fn().mockReturnValue('mocked_value'),
},
};
});
Jest configuration needs to contain Babel transform, transform
option needs to be removed in order to use default value.
The project needs to contain correct Babel configuration, e.g. babel.config.json:
{
"presets": ["@babel/preset-env"]
}
This also requires @babel/preset-env
package to be installed.
Either native ESMs need to be disabled by removing "type": "module"
in package.json, or --experimental-require-module
option needs to be used to run Jest:
node --experimental-require-module node_modules/jest/bin/jest.js
Upvotes: 1