jestjs / jest

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

jest.mock does not mock an ES module without Babel #10025

Open aldeed opened 4 years ago

aldeed commented 4 years ago

πŸ› Bug Report

In an ES module Node project, with no Babel, jest.mock works when the mocked module is a node_modules package that exports CommonJS, but it isn't working for me mocking an ES module exported from a file in the same project.

(It's possible that an NPM package that only exports ES modules has the same issue. I didn't try that case.)

To Reproduce

Steps to reproduce the behavior:

Click Run in the repl, or here's a simple example:

// main.js
import secondary from "./secondary.js";

export default function main() {
  return secondary();
}

// secondary.js
export default function secondary() {
  return true;
}

// test.js
import { jest } from "@jest/globals";

jest.mock("./secondary.js");

let main;
let secondary;
beforeAll(async () => {
  ({ default: main } = await import("./main.js"));
  ({ default: secondary } = await import("./secondary.js"));
});

test("works", () => {
  secondary.mockReturnValueOnce(false); // TypeError: Cannot read property 'mockReturnValueOnce' of undefined
  expect(main()).toBe(false);
});

Expected behavior

jest.mock(filename) should mock the exports from filename when the test file and the Node project are both ES modules (type: "module")

Link to repl or repo (highly encouraged)

https://repl.it/repls/VerifiableOfficialZettabyte

envinfo

  System:
    OS: macOS 10.15.4
    CPU: (4) x64 Intel(R) Core(TM) i5-4278U CPU @ 2.60GHz
  Binaries:
    Node: 12.16.3 - ~/.nvm/versions/node/v12.16.3/bin/node
    Yarn: 1.21.1 - /usr/local/bin/yarn
    npm: 6.14.4 - ~/DevProjects/reaction/api-utils/node_modules/.bin/npm
  npmPackages:
    jest: ^26.0.1 => 26.0.1 
aldeed commented 4 years ago

Copied from https://github.com/facebook/jest/issues/9430#issuecomment-625418195 at the request of @SimenB. Thanks!

aldeed commented 4 years ago

I respectfully disagree with this being labeled a feature request. It's entirely blocking of any effort to move to Jest native ES module support if any files have mocks in them, and there is no workaround that I know of (other than to continue using CommonJS through Babel, which means that ES module support is broken, hence bug).

SimenB commented 4 years ago

I started working on this, and I think it makes sense to leave .mock and .doMock for CJS, and introduce a new .mockModule or something for ESM. It will require users to be explicit, and allow the factory to be async. Both of which I think are good things.

Also need to figure out isolateModules. Unfortunately it uses the module name while not being for ES modules.

@thymikee @jeysal thoughts?

guilhermetelles commented 4 years ago

@aldeed @SimenB Hello, I'm also having the same problem, but when I try to use jest with babel instead, I'm running into SyntaxError: Cannot use import statement outside a module (it throws inside an imported module), which is basically what #9430 is all about I guess.

Is there any workaround to mock modules from the same project? Or to prevent the SyntaxError from occurring when using babel .

aldeed commented 4 years ago

@guilhermetelles It can be a pain to do, but you'll likely get more help if you create a public minimal reproduction repo for your issue and create a new GH issue that references this one. There are about a dozen things that could cause this issue, from using an older Node version to Babel config issues, and being able to see all the project files is the best way for someone to help you solve it.

@SimenB You mentioned above that you had a start on this and it looks like everyone πŸ‘ your proposal. Is there any update?

SimenB commented 3 years ago

One thing to note is that it will be impossible to mock import statements as they are evaluated before any code is executed - which means it's not possible to setup any mocks before we load the dependency. So you'll need to do something like this using import expressions.

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

jest.mockModule('someModule', async () => ({ foo: 'bar' }));

let someModule;

beforeAll(async () => {
  someModule = await import('someModule');
});

test('some test', () => {
  expect(someModule.foo).toBe('bar');
});

It will be a bit cleaner with top-level await

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

jest.mockModule('someModule', async () => ({ foo: 'bar' }));

const someModule = await import('someModule');

test('some test', () => {
  expect(someModule.foo).toBe('bar');
});

Any modules loaded by someModule via import statements would work though as we'd have time to setup our mocks before that code is evaluated.


The example in the OP follows this pattern, I'm just pointing it out πŸ‘

stellarator commented 3 years ago

@SimenB this is my first comment in this repo, so the first words unambiguously look like - great work!

We're in one step from transitioning of our infrastructure into ESM. The only things left are tests. We're planning to actively use top level await in our code base and there is an obstacle because we're ready to compile our code to ESNext (in terms of TS) but most of our tests use jest.mock() somehow and I want to understand the current state of affairs.

After closing #9860 by #10823 there is one important topic left considering original list in #9430 - support of jest.(do|un)mock (OK, to be honest, probably there is another one - Package Exports support).

Can You explain the current status of the issue?! I mean:

Thanks in advance.

SimenB commented 3 years ago

I want to add jest.mockModule, but since that's a new API and not a breaking change it might not go into Jest 27 at release. A PR would be very much welcome, but it's getting into some of the nitty gritty of jest-runtime so I understand if people are a bit hesitant to attempt it πŸ™‚

SimenB commented 3 years ago

As a status update, I've opened up a PR here: #10976

anshulsahni commented 3 years ago

@SimenB before your PR gets merged, what is the work-around solution here?

kalinchernev commented 3 years ago

@anshulsahni no workaround, apart from rewriting your code to not use mocking for the moment with the esm modules

SimenB commented 3 years ago

yeah, there is not workaround if you wanna use native ESM until that lands

anshulsahni commented 3 years ago

right now I'm using babel & it's configured to only convert esm modules. So the whole app runs in esm modules but tests run with commonjs module system

SimenB commented 3 years ago

yep, that'll keep working

mahnunchik commented 3 years ago

I'm looking forward for this feature πŸ‘

victorgmp commented 3 years ago

right now I'm using babel & it's configured to only convert esm modules. So the whole app runs in esm modules but tests run with commonjs module system

you can show me your config please? my apps runs in esm modules and I need run my test with commonjs modules

scamden commented 3 years ago

@anshulsahni no workaround, apart from rewriting your code to not use mocking for the moment with the esm modules

one quasi work around is to use rushstack's heft tool to compile your ts. They allow a secondary emit target so you can emit cjs and esm but with only one compiler pass. pretty slick way to handle it until this is supported if you ask me.

https://rushstack.io/pages/heft_configs/typescript_json/

AlvinLaiPro commented 3 years ago

marked

JakobJingleheimer commented 3 years ago

There IS (sometimes) a workaround, but it requires you to go about things in a very specific way:

outdated: there's a better way (below) ```js // babyMaker.mjs import * as cp from 'child_process'; export default function babyMaker() { cp.default.fork(/* … */); } ``` ```js // babyMaker.test.js import babyMaker from './implementation.mjs'; import * as cp from 'child_process'; beforeAll(() => { cp.default.fork = jest.fn(() => ({}); }); ``` In the above, `cp.default.fork` is a mock that returns an empty object. Note that `cp.fork` cannot be mocked this way because it is a direct export (which ESM protects), and even when `cp.default.fork` _is_ mocked, `cp.fork` still is NOT because `cp.default.fork` has been re-assigned to the mock; its original value (the named export `fork`) is unaffected. Under the hood, the child_process module is doing something like ```js export function fork() {} export default { fork, // …others }; ``` Note that child_process _is_ CJS under the hood, but that doesn't matter: `export default {…}` works from ESM because the default export is an object whose properties are not protected.
// foo.mjs

function bar() {
  // …
}

export default {
  bar,
};
// qux.mjs

import foo from 'foo.mjs';

export default qux() {
  const something = foo.bar();

  // …
}
// qux.test.mjs

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

 // MUST be imported BEFORE qux
const foo = await import('foo.mjs')
  .then(({ default: foo }) => Object.defineProperties(foo, {
    bar: { value: jest.fn() }, // overwrite foo's `default.bar` with the mock
  }));

// MUST be after any/all mocks
const qux = await import('qux.mjs')
  .then(({ default: d }) => d);

This works because ESM does not protect properties of exported objects, only the export itself.

For the mock to be present in the top-level scope (eg when imported), you must use await import in the test file to enforce sequence (including the qux import!).

thernstig commented 3 years ago

I have mocked node-fetch and import it to a test file with import * as fetch from 'node-fetch'; - but the functions I have set in the mock such as fetch.setMockJsonResponse cannot be found in the test file. Is that the same issue as this?

JakobJingleheimer commented 3 years ago

@thernstig probably yes. See my comment above for a potential workaround.

yurijmikhalevich commented 3 years ago

If anyone is interested in giving jest.mockModule from this PR β€” https://github.com/facebook/jest/pull/10976 β€” a try right now, here is a small example using it: https://github.com/yurijmikhalevich/esm-project-with-working-jest-mock.

damianobarbati commented 3 years ago

@yurijmikhalevich is that going to be merged anytime soon?

emilioSp commented 3 years ago

@yurijmikhalevich do you have any update, please?

SimenB commented 3 years ago

https://github.com/facebook/jest/issues/9430#issuecomment-915109139

jasonrberk commented 2 years ago

so is it safe to say, that the ONLY way you can actually do this: https://jestjs.io/docs/mock-functions#mocking-modules is by using babel to convert everything back to CommonJS? I'm so against having to involve babel in my relatively small project, i think it would just be easier to convert the entire project back to CommonJS and just wait for the testing frameworks to catch up.

please correct me if I'm wrong in my understanding....

also, as an aside, it would be really helpful to have a disclaimer on https://jestjs.io/docs/mock-functions#mocking-modules that you'll still need babel for your pure Node project before you build the whole thing and then see this:

https://jestjs.io/docs/ecmascript-modules

GerkinDev commented 2 years ago

@jasonrberk Fact is, if you use jest, you use babel already: https://jestjs.io/docs/next/code-transformation#defaults.

Depending on your complete stack, it can be not that hard to mock modules. With typescript, ts-jest includes presets that requires just a bit of config for allow it to handle node_modules if required. Then, to work with the hoisting mechanism of babel-jest that is broken with ESM (since static imports are resolved before running the script for what I've seen), you just have to use top-level await to import files importing the modules to mock.

Mock before async imports and do not import statically anything that may import some-dep statically

Example plagiating @SimenB above: You have the following structure: * src * foo.ts * foo.spec.ts * node_modules * some-dep In `src/foo.ts`: ```ts import { depFn } from 'some-dep'; export const doStuff = (...args: any[]) => depFn(...args); ``` In `src/foo.spec.ts`: ```ts import { jest } from '@jest/globals'; // Order is important. jest.mock('some-dep', () => ({ depFn: jest.fn() }); const { doStuff } = await import('./foo'); const { depFn } = await import('some-dep'); it('should pass args to `depFn`', () => { doStuff('a', 'b'); expect(depFn).toHaveBeenCalledWith('a', 'b'); }); ```

Long story short: you use jest, you use babel if you don't explicitly disable transforms, AFAIK. It does most of the heavy lifting out of the box already.

jasonrberk commented 2 years ago

@GerkinDev - 🀯

first off, thanks for the info.....

so if follow, you guys are working to fix the babel that jest is using under the covers to support using ESM in our own code, instead of just stuff imported from node_modules?

In the meantime, I could

A) do some babel stuff myself (which would happen in place of the transform Jest is doing) and convert all my js files to CommonJS via custom babel?

B) just use CommonJS for now in my source and switch it all to ESM later, once the guys with the big brain fix the issue(s)

thanks for helping me out.....been a long time since I did anything in node / js

jasonrberk commented 2 years ago

@GerkinDev

I still can't get the provided complete example working

https://github.com/jasonrberk/jest-mocking-esm

and I still don't get if I'm responsible for doing something with Babel, or if that's all buried behind jest????

what am I doing wrong here: https://github.com/jasonrberk/jest-mocking-esm

GerkinDev commented 2 years ago

As I mentioned above, it depends on your setup: some other preprocessors, like ts-jest, makes the thing much easier somehow (I just tried to dig in and didn't found a reason. Maybe somebody else here knows how it does it almost like magic).

In your case, in pure ESM, you can replace the beginning of your test file like the following:

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

const mockPlaySoundFile = jest.fn();
jest.unstable_mockModule('./sound-player', () => {
    return {default: jest.fn().mockImplementation(() => {
        return { playSoundFile: mockPlaySoundFile };
    })};
});
const {default: SoundPlayer} = await import('./sound-player');
const {default: SoundPlayerConsumer} = await import('./sound-player-consumer');

beforeEach(() => {
  SoundPlayer.mockClear();
  mockPlaySoundFile.mockClear();
});

Sorry for the confusion, I was confused by ts-jest doing stuff I didn't expect it to do, that made things work in my tests here.

mattfysh commented 2 years ago

Hi all, great work on the ESM implementation so far. Only took a couple of hours to get up and running with a project that uses node natively, with no code transforms

I've found an issue with jest.unstable_mockModule where the factory function is called once per import of mocked module found in the code.

So if I have two files, both importing the mocked module, they'll each get a separate instance. And then this can cause issues if importing the mocked module into the test case, it's not guaranteed which of the instances you will get. In my case, the instance imported into my test file is not the one that contains the right data in .mock.calls

So far the workaround is to reorder a few imports in my source code, such that the module making the call to the mocked module is imported last.

frank-dspeed commented 2 years ago

out of my view the mock implementation of jest simply needs a diffrent api for esm as it needs to address shared linked modules also!

import('./my-module.mjs').then(moduleToMock => mock(moduleToMock.get))
import('./my-module.cjs').then(moduleToMock => mock(moduleToMock.default))

i think the mock implementation that now trys to find a object via a string mock('moduleToMock') is confusing anyway

simply taking a object and returning a proxy for it is enough to implement mocking.

scottdotweb commented 2 years ago

@GerkinDev Hello, I'm experimenting with using pure ESM for testing, and find myself in the same situation as @jasonrberk. However, your suggestion isn't working for me to mock a module (fs in this case) that's being loaded by the module being tested.

Here's a minimal example:

import { jest } from '@jest/globals'

const mockReadFileSync = jest.fn().mockImplementation(() => {
  return { version: '1.0' }
})

jest.unstable_mockModule('fs', () => {
  return {
    default: jest.fn().mockImplementation(() => {
      return { readFileSync: mockReadFileSync }
    })
  }
})

import { getVersion } from 'utils.js'

it('gets the app\'s version', () => {
  expect(getVersion()).toBe('1.0')
})

This test fails - the real fs gets used, not the mock. Do you have any idea why that might be the case? Thanks!

Edit: I dug around a bit more and see that this PR is supposed to have replaced jest.unstable_mockModule with jest.mockModule in 27.1.1. However, if I try to use that in my test with 27.4.7, I get "jest.mockModule is not a function", which is confusing. It feels like that probably relates to this problem, though.

vialoh commented 2 years ago

I was unable to get TypeScript + ESM + mocks to work and ended up transpiling to CJS with Babel for tests.

For anyone still trying to have testable code with an ESM codebase (i.e., { "type": "module" } in package.json), maybe my config will work for you too.

Install the dependencies:

npm i -D @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript babel-jest babel-plugin-transform-import-meta

If you aren't using TypeScript, then you of course don't need to install @babel/preset-typescript.

jest.config.json:

{
  "roots": [
    "<rootDir>/src/"
  ],
  "setupFiles": [
    "<rootDir>/src/environment.ts"
  ],
  "moduleNameMapper": {
    "^(\\.{1,2}/.*)\\.jsx?$": "$1"
  }
}

The setupFiles are specific to my setup. You can probably remove or update that for your own.

moduleNameMapper strips the .js and .jsx extensions from module names, which is necessary because ESM import statements need the file extension, while the require statements produced by Babel do not.

babel.config.json:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/transform-runtime",
    "babel-plugin-transform-import-meta"
  ]
}

If you aren't using TypeScript, then you of course don't need @babel/preset-typescript.

If you're using ESM, you'll probably use import.meta.url at some point, which is unavailable in CJS code. The two Babel plugins above transform import.meta.url statements into something CJS can use.

For example, maybe you need __dirname, which is unavailable in ESM:

import { fileURLToPath } from 'url'
import path from 'path'

const __dirname = fileURLToPath(path.dirname(import.meta.url))

Or maybe you want to check if the module was the entry point for the Node process:

import process from 'process'
import { fileURLToPath } from 'url'

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  // This module was the entry point.
}
scottdotweb commented 2 years ago

Following some advice I saw somewhere (hard to know... I've had so many tabs open today chasing this) I installed @babel/plugin-transform-modules-commonjs to turn ESM into CommonJS in the test environment, but it doesn't work - it dies with SyntaxError: The requested module 'whatever.js' does not provide an export named 'someFunction'. Someone's filed a bug for that as #12120. I'm considering my options at this point.

vialoh commented 2 years ago

@scottdotjs Have you tried the setup I described above?

scottdotweb commented 2 years ago

@vialoh None of that was relevant to my situation. Also this is incorrect:

moduleNameMapper strips the .js and .jsx extensions from module names, which is necessary...

Jest is able to see the modules without mapping the file names, as my comment above would suggest.

vialoh commented 2 years ago

@scottdotjs What is different about your setup?

Also, regarding the moduleNameMapper, does your code import someModule from './someModule.js' (with the .js extension) or does it import someModule from './someModule (without the extension)? If it's the latter then it isn't actually following the ES modules spec.

frank-dspeed commented 2 years ago

@vialoh sorry your wrong "string" specifier are part of the spec they get resolved via the importMap proposal in userland ESM Code and else its up to the Environment to resolve string module Specifiers

they are also implemented in NodeJS via packagejson import fild replacing partial importMaps

vialoh commented 2 years ago

@frank-dspeed Can you show me how to do what you're talking about in Node without needing to map every local import? Is there something that can natively automatically add .js (and possibly other extensions) to extensionless local imports? I was unable to find a way to do this natively but maybe I wasn't looking hard enough.

To quickly try what I'm talking about:

mkdir esm-import-example
cd esm-import-example
npm init

Add "type": "module" to package.json.

Create foo.js:

export const foo = 'bar'

Create index.js:

import { foo } from './foo'

console.log(foo)

Run node index.js and you'll be met with the following error:

$ node index.js
node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'esm-import-example/foo' imported from esm-import-example/index.js
Did you mean to import ../foo.js?
←[90m    at new NodeError (node:internal/errors:371:5)←[39m
←[90m    at finalizeResolution (node:internal/modules/esm/resolve:416:11)←[39m
←[90m    at moduleResolve (node:internal/modules/esm/resolve:932:10)←[39m
←[90m    at defaultResolve (node:internal/modules/esm/resolve:1044:11)←[39m
←[90m    at ESMLoader.resolve (node:internal/modules/esm/loader:422:30)←[39m
←[90m    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:222:40)←[39m
←[90m    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:76:40)←[39m
←[90m    at link (node:internal/modules/esm/module_job:75:36)←[39m {
  code: ←[32m'ERR_MODULE_NOT_FOUND'←[39m
}
frank-dspeed commented 2 years ago

@vialoh sure here you go from https://nodejs.org/api/packages.html#subpath-patterns

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*": "./src/features/*.js"
  },
  "imports": {
    "#internal/*": "./src/internal/*.js"
  }
}

All instances of * on the right hand side will then be replaced with this value, including if it contains any / separators.

import featureX from 'es-module-package/features/x';
// Loads ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y';
// Loads ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z';
// Loads ./node_modules/es-module-package/src/internal/z.js
vialoh commented 2 years ago

@frank-dspeed Interesting, thanks!

I was able to get your suggestion to work with the following...

src/foo.js:

export const foo = 'bar'

src/index.js:

import { foo } from '#foo'

console.log(foo)

package.json:

{
  "name": "esm-import-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "imports": {
    "#*": "./src/*.js"
  },
  "author": "",
  "license": "MIT"
}

I'm not sure how I feel about it though. It feels sort of wrong, maybe from too many years of using relative imports. Although this may actually prove to be better for modularity in the long term.

I am still curious though. Is it possible to import using a relative path (e.g., import ./foo) instead of needing the path prefixed with #?

For example...

src/index.js:

import { foo } from './foo'

console.log(foo)

package.json:

{
  "imports": {
    "./*": "./*.js"
  }
}
frank-dspeed commented 2 years ago

the prefix # was fully optional i showed both the # is a internal convention to easy parse and find such imports without much trouble

and about your other question It is fully environment dependent the NPM way works only for NodeJS as it is from the same People. The Cross Platform way is ImportMaps you can use them in many environments today

and the offical shim PoillyPony Fill for that is systemjs a cross platform module system that offers everything that ESM Specs try to enforce on environments.

for example the nodejs modules will now get referenced like that in future

import { writeFile } from 'node:fs/promises'
vialoh commented 2 years ago

@frank-dspeed I used SystemJS a while back, but I'm trying to avoid relying on any fills or transpilation.

If I have src/foo.js and src/index.js and want to import { foo } from './foo' (exactly as written here) from within src/index.js, what would the package.json configuration be?

PeterKDex commented 2 years ago

Is a fix for this still on the roadmap?

PeterKDex commented 2 years ago

Hey all, here is what I decided to do, in case anyone finds it useful... I'm allowing manual dependency injection into my module functions via arguments, to allow for mocks, but setting default argument value to the normally imported dependency. I personally feel like this is reasonable given the circumstances. Example:

//my-module.js
import { myDependency } from './dependencies';
export function doStuff(arg1, arg2, dep_myDependency = myDependency) {
    // TODO: Use dep_myDependency()
}

//my-module.test.js
import { jest, expect } from '@jest/globals';
import { doStuff } from './my-module.js';
test('Does stuff', () => {
    const mockMyDependency = jest.fn();
    doStuff('foo', 'bar', mockMyDependency);
    // Regular app usage: doStuff('foo', 'bar')
    // Expect, etc...
}
jurijzahn8019 commented 2 years ago

Yeah! The true C# way. But automocking was soooooo convenient πŸ˜ƒ

frank-dspeed commented 2 years ago

@PeterKDex exactly that is what you should do it is always a good pattern to allow your module dependencies to be inject able.

ghost commented 2 years ago

Has this been fixed in v28.0.0?

jayfunk commented 2 years ago

I was unable to get TypeScript + ESM + mocks to work and ended up transpiling to CJS with Babel for tests.

For anyone still trying to have testable code with an ESM codebase (i.e., { "type": "module" } in package.json), maybe my config will work for you too.

Install the dependencies:

npm i -D @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript babel-jest babel-plugin-transform-import-meta

If you aren't using TypeScript, then you of course don't need to install @babel/preset-typescript.

jest.config.json:

{
  "roots": [
    "<rootDir>/src/"
  ],
  "setupFiles": [
    "<rootDir>/src/environment.ts"
  ],
  "moduleNameMapper": {
    "^(\\.{1,2}/.*)\\.jsx?$": "$1"
  }
}

The setupFiles are specific to my setup. You can probably remove or update that for your own.

moduleNameMapper strips the .js and .jsx extensions from module names, which is necessary because ESM import statements need the file extension, while the require statements produced by Babel do not.

babel.config.json:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/transform-runtime",
    "babel-plugin-transform-import-meta"
  ]
}

If you aren't using TypeScript, then you of course don't need @babel/preset-typescript.

If you're using ESM, you'll probably use import.meta.url at some point, which is unavailable in CJS code. The two Babel plugins above transform import.meta.url statements into something CJS can use.

For example, maybe you need __dirname, which is unavailable in ESM:

import { fileURLToPath } from 'url'
import path from 'path'

const __dirname = fileURLToPath(path.dirname(import.meta.url))

Or maybe you want to check if the module was the entry point for the Node process:

import process from 'process'
import { fileURLToPath } from 'url'

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  // This module was the entry point.
}

@vialoh I went with your approach to get around the lack of mocking. It looks like it worked out just fine. I will report back if I find any issues. I think this is the simplest approach until there is some kind of official solution from the Jest team.