admin管理员组

文章数量:1241129

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.

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.

Share Improve this question edited 2 days ago Alex asked 2 days ago AlexAlex 312 silver badges7 bronze badges 8
  • What you do could work. This depends on your setup, which isn't shown. Please, provide a way to reproduce – Estus Flask Commented 2 days ago
  • github/aexel90/jest_mock – Alex Commented 2 days ago
  • codesandbox.io/p/github/aexel90/jest_mock/main – Alex Commented 2 days ago
  • You need to use babel transform in order to be able to mock modules in unrestricted way. Currently you're using native esm, see jestjs.io/docs/ecmascript-modules – Estus Flask Commented 2 days ago
  • What does it mean - unrestricted way? Do you have a working example? Or could you fork the repository and propose a solution. Much appreciated. – Alex Commented 2 days ago
 |  Show 3 more comments

3 Answers 3

Reset to default 1

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

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.

Issue

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:

  1. In service.js, exporting a singleton instance of the Service class:
    export default new Service();
    
  2. In main.js, the service import is directly using that singleton:
    import service from './service';
    
  3. In 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.

Solution with jest.mock

https://github/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');
    });
});

Solution with jest.unstable_mockModule

https://github/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

Solution with jest.unstable_mockModule and changing the singleton during test execution

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

本文标签: javascriptJest mock singleton instanceStack Overflow