jestjs / jest

Delightful JavaScript Testing.
https://jestjs.io
MIT License
43.85k stars 6.39k forks source link

[Bug]: Mocking Partials (in with ES6 modules) doesn't work as per documentation #15100

Open harvzor opened 1 month ago

harvzor commented 1 month ago

Version

29.7.0

Steps to reproduce

Stackblitz playground: https://stackblitz.com/edit/stackblitz-starters-nybxbq

Result:

image


Follow the documentation available here: https://jestjs.io/docs/mock-functions#mocking-partials

  1. Create a file called ./foo-bar-baz.js:
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
  1. Create a file called ./tests/test.js:
import defaultExport, { bar, foo } from '../foo-bar-baz';
import { jest } from '@jest/globals';

jest.mock('../foo-bar-baz', () => {
  const originalModule = jest.requireActual('../foo-bar-baz');

  //Mock the default export and named export 'foo'
  return {
    __esModule: true,
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport).toHaveBeenCalled();

  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});
  1. Make sure the package.json is configured correctly (so Jest works with ES6 modules):
{
  "name": "jest-starter",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  },
  "devDependencies": {
    "jest": "^29.7.0"
  },
  "jest": {
    "transform": {}
  }
}
  1. Run the test with npm test
  2. Inspect results

Expected behavior

Actual behavior

Test fails, and it appears the mocks simply aren't run:

❯ npm test

> jest-starter@0.0.0 test
> node --experimental-vm-modules node_modules/jest/bin/jest.js

(node:32) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 FAIL  ./test.js
  ✕ should do a partial mock (5 ms)

  ● should do a partial mock

    Expected value   undefined
    Received:
      undefined

    Message:
      expect(received).toBe(expected) // Object.is equality

    Expected: "mocked baz"
    Received: "baz"

    Difference:

    Compared values have no visual difference.Error:

      at processResult (node_modules/expect/build/index.js:284:19)
      at throwingMatcher (node_modules/expect/build/index.js:336:16)
      at null.<anonymous> (test.js:29:31)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.976 s, estimated 1 s
Ran all test suites.

Additional context

Pulling down the StackBlitz and running it locally produces the same result.

Also, in my own repository, I have this same issue.

Environment

This is what my local environment reports:

  System:
    OS: Windows 10 10.0.19045
    CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
  Binaries:
    Node: 18.20.3 - C:\Program Files\nodejs\node.EXE
    npm: 10.7.0 - C:\Program Files\nodejs\npm.CMD
  npmPackages:
    jest: ^29.7.0 => 29.7.0

This is what StackBlitz reports:

  System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
  Binaries:
    Node: 18.20.3 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.15.6 - /usr/local/bin/pnpm
  npmPackages:
    jest: ^29.7.0 => 29.7.0 
harvzor commented 1 month ago

This works:

import { jest } from '@jest/globals';

jest.unstable_mockModule('../foo-bar-baz', () => {
  return {
    bar: () => 'mocked bar',
  };
});

const { bar } = await import('../foo-bar-baz');

test('should do a partial mock', () => {
  expect(bar()).toBe('mocked bar');
});

Docs for unstable_mockModule are available here: https://jestjs.io/docs/ecmascript-modules#module-mocking-in-esm

Trying to make the implementation exactly the same as in the original example, I think this is the correct code:

import { jest } from '@jest/globals';

jest.unstable_mockModule('../foo-bar-baz', async () => {
  const originalModule = await import('../foo-bar-baz');

  return {
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

const { default: defaultExport, bar, foo } = await import('../foo-bar-baz');

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport).toHaveBeenCalled();

  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});

However, the test never finishes. Seems this issue was reported a while ago: https://github.com/jestjs/jest/issues/13851

mrazauskas commented 1 month ago

As you see CJS has jest.requireActual(), but it can’t work in ESM. Similar async method for imports is needed as mentioned in the meta issue: #9430

harvzor commented 1 month ago

It seems that https://github.com/jestjs/jest/issues/10025 is tracking a potential fix for this issue (since 2020)

Since requireActual() does not work with partial ES6 module mocks, probably the documentation should say this.

Should I make a PR?

harvzor commented 1 month ago

Similar code works in Vitest using a special method called importOriginal(): https://stackblitz.com/edit/vitejs-vite-fcr3z9

Vitest supports partial mocks but has a caveat that's well explained in the docs:

It is not possible to mock the foo method from the outside because it is referenced directly. So this code will have no effect on the foo call inside foobar (but it will affect the foo call in other modules):

And then goes on to write:

This is the intended behaviour. It is usually a sign of bad code when mocking is involved in such a manner.

https://vitest.dev/guide/mocking.html#mocking-pitfalls

Probably partial mocks should be generally avoided.

mrazauskas commented 1 month ago

require() does not work in ESM as well as requireActual(). It might make sense to mention that partial mocking is not yet supported, but that is mentioned in #9430 and the ECMAScript Modules page already links to this issue. In a way several features are not yet working. Not sure if it worth mentioning them in documentation.

As a side note. Jest and Vitest have very different approach to ESM. In Jest a plain JavaScript ESM is simply executed, in Vitest you will see transform time being reported. I don’t know what is that transformation about, but that is: 1. waste of time; 2. not ESM as implemented by Node.js (for instance require global gets injected).

github-actions[bot] commented 1 week ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.