ldiqual
ldiqual

Reputation: 15365

Jest: Standard way to stub named exports of ESM modules

This question is not specific to Jest as it applies to all testing libraries with stubbing capabilities.

ESM modules have immutable named and default exports, which means this is no longer valid:

// @filename foo.mjs
export function foo() { ... }

// @filename foo.test.mjs
import * as foo from './foo.mjs'
// Causes runtime error because named export "foo" is immutable
jest.spyOn(foo, 'foo')

What is the current "standard" way to spy/mock named exports with ESM modules?

Potential solutions

Delegate to mutable object

// @filename foo.mjs
export function foo() {
  return _private.foo()
}
export const _private = {
  foo: () => { ... }
}

// @filename foo.test.mjs
import { _private } from './foo.mjs'
jest.spyOn(_private, 'foo')

Wrap function in proxy

// @filename proxy.mjs
export function proxy(fn) {
  const functionProxy = function (...args) {
    return functionProxy._original(...args)
  }
  functionProxy._original = fn
  return functionProxy
}

// @filename foo.mjs
import { proxy } from './proxy.mjs'
export const foo = proxy(() => { ... })

// @filename foo.test.mjs
import { foo } from './foo.mjs'
jest.spyOn(foo, '_original')

Upvotes: 6

Views: 1155

Answers (2)

Andrey Smolko
Andrey Smolko

Reputation: 1154

According the ES specification a module namespace object has:

Each such property has the attributes { [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false }.

spyOn method try to reassign (if a property is an own property) or add a new one (if a property in a proto object) - https://github.com/facebook/jest/blob/main/packages/jest-mock/src/index.ts#L1112

In spite of a writable attribute you can not reassign a property of namespace object(special nature of namespace object) or add a new one as namespace object is not extensible.

But the fact that a property is writable allows to apply a next trick:

// @filename foo.mjs
export function foo() { ... }

// @filename foo.test.mjs
import * as foo from './foo.mjs'
const p_foo = Object.create(foo)
// Do not cause runtime error because spyOn copies a method from proto in object itself 
jest.spyOn(p_foo, 'foo')

Honestly I have never used that trick, but I see here a plus as no need to updated existing source code, just add an additional wrapper for a namespace object. But that approach is coupled with info from spyOn method implementation.

Upvotes: 0

Tom
Tom

Reputation: 9127

Not really supported by Jest

The standard approach to mock parts of other modules is to use jest.mock, like so:

import * as foo from './foo.mjs'

jest.mock('./foo.mjs', () => ({
  foo: () => {
    console.log(`I AM FAKE FOO`)
    return 4
  }
}))

In a CommonJS environment, jest.mock hijacks the require function so that dependencies loaded by the code under test will be replaced with your mocks as desired.

Unfortunately, Jest hasn't yet figured out how to do this in an ES6 module environment. From Jest's mocking page:

Please note that we currently don't support jest.mock in a clean way in ESM, but that is something we intend to add proper support for in the future. Follow this issue for updates.

It seems like the bottom line is that you will not be able to do what you're trying to do with Jest.


You say:

This question is not specific to Jest as it applies to all testing libraries with stubbing capabilities.

... but, like it not, your question is specific to Jest. Testing libraries like Jest accomplish their work by doing strange things, not by leveraging existing language features. That means there isn't some arcane ES6 capability that they all use that you could learn about here. Each of them uses an invented solution, and as far as I'm aware they all work by modifying the execution environment.

Upvotes: 3

Related Questions