Skip to content
iambumblehead edited this page Sep 13, 2022 · 110 revisions
+                                                β–ˆβ–ˆβ•—
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•—
+β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•”β•
+β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
+β–ˆβ–ˆβ•”β•β•β•β•β•β• β•šβ•β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
+β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β•šβ–ˆβ–ˆβ•—
+ β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β•  β•šβ•β•  β•šβ•β• β•šβ•β•β•β•β•β•  β•šβ•β•β•β•β•β•β•šβ•β•   β•šβ•β•

esmock provides native ESM import mocking on a per-unit basis.

Install

$ npm install --save-dev esmock

After esmock is installed, update your package.json "test" script to use --loader=esmock

{
  "name": "give-esmock-a-star",
  "type": "module",
  "scripts": {
    "test": "node --loader=esmock --test",
    "test-mocha": "mocha --loader=esmock",
    "test-tap": "NODE_OPTIONS=--loader=esmock tap",
    "test-ava": "NODE_OPTIONS=--loader=esmock ava",
    "test-uvu": "NODE_OPTIONS=--loader=esmock uvu spec",
    "test-tsm": "node --loader=tsm --loader=esmock --test *ts",
    "test-ts": "node --loader=ts-node/esm --loader=esmock --test *ts",
    "test-jest": "NODE_OPTIONS=--loader=esmock jest"
  },
  "jest": {
    "runner": "jest-light-runner"
  }
}

Hello World

import test from 'node:test'
import assert from 'node:assert/strict'
import esmock from 'esmock'

test('should mock hello world', async () => {
  const hello = await esmock('../src/hello.js', {
    '../src/icons': { world: '🌎' }
  })
  
  assert.strictEqual(hello('world'), '🌎')
})

Decide Strict or Partial

Before using esmock, decide which mocking type suits your tests β€”"strict mocking" or "partial mocking".

Strict mocking replaces original module definitions with mock definitions

mockStrict = mockDefs

Partial mocking merges original module definitions with mock definitions

mockPartial = Object.assign(await import('/module.js'), mockDefs)

Strict mocking is recommended for safety and clarity. It forces the test context to explicitly define each mocked module definition imported to the mocked import tree. When exported names are renamed in source files, mocking test files must be updated with those names. Both requirements are seen as features by advocates of strict mocking.

Partial mocking is recommended for brevity and simplicity. It allows the test context to define only test-relevant mocked-module definitions. For some situations, it makes sense to import and export all original definitions, while only stubbing those specifically mocked definitions.

Neither strict nor partial mocking are specifically recommended by esmock's author. esmock supports both strict and partial mocking and uses partial mocking by default,

  • to use "partial mocking" import and use esmock(...args)
  • to use "strict mocking" import { strict as esmock } from 'esmock' or use esmock.strict(...args)

Calling esmock

esmock returns an imported module with an import tree composed of original and mock definitions,

await esmock(
  './to/module.js', // filepath to the source module being tested
  { ...childmocks }, // mock definitions imported by the source module, directly
  { ...globalmocks }) // mock definitions imported anywhere in the source import tree

Child and global "mocks" passed to esmock are both defined the same way,

const mocks = {
  // for a package
  eslint: { ESLint: cfg => cfg },

  // for a core module
  fs: { readFileSync: () => 'returns this globally' },

  // for a local file
  '../src/main.js': { start: 'go' },

  // for a subpath
  '#feature': { release: () => 'release time' }

  // with default and/or named exports
  '../src/hello.js': {
    default: () => 'world',
    exportedFunction: () => 'foo'
  },

  // with short-hand default exports
  '../src/init.js': () => 'init'
}

Calling esmock, basic

This example mocks a local file and a package,

/src/main.js

import serializer from 'serializepkg'
import someDefaultExport from './someModule.js'

export default () => serializer(someDefaultExport())

/tests/main.js

test('should mock modules and local files', async t => {
  const main = await esmock('../src/main.js', {
    serializepkg: {
      default: obj => JSON.stringify(obj)
    },
    '../src/someModule.js' : {
      default: () => ({ foo: 'bar' })
    }
  })

  // Because `serializepkg` is mocked as a function calling JSON.stringify()
  // and `someDefaultExport` is mocked as a function returning { foo: 'bar' }
  t.is(main(), JSON.stringify({ foo: 'bar' }))
})

Calling esmock, strict

Use esmock.strict or import { strict } from 'esmock' to use strict mocking that replaces (rather than merges) original module definitions with mock definitions. To demonstrate the difference, a target module and its usage with esmock,

logpath.js

import p from 'path'

console.log({
 dir: p.dirname('/dog.png'),
 base: p.basename('./dog.png')
})

logpath.test.js

import esmock, { strict } from 'esmock'

esmock('./logpath.js', { path: { basename: () => 'cat.png' } })
// "dir: /, base: cat.png"

strict('./logpath.js', { path: { basename: () => 'cat.png' } })
// Error "The requested module 'path' does not provide an export named 'dirname'"

esmock.strict('./logpath.js', { path: { basename: () => 'cat.png' } })
// Error "The requested module 'path' does not provide an export named 'dirname'"

The "strict mocking" behaviour uses the mock definition with "basename" only and "dirname" is never defined, resulting in a runtime error.

Calling esmock, await import

Use esmock.p(...) to mock await import'd modules. When esmock.p is used, mock definitions are saved and returned later, when a module is imported dynamically. For example, let's use esmock.p() to test a different file that calls await import('eslint'),

loadConfig.js

export default async function loadConfig (config) {
  const eslint = await import('eslint')

  return new eslint.ESLint(config)
}

loadConfig.test.js

test('should mock module using `async import`', async t => {
  const loadConfig = await esmock.p('./loadConfig.js', {
    eslint: { ESLint: o => o }
  })

  t.is(await loadConfig('config'), 'config')

  // the test runner ends each test process to free memory
  // you probably don't need to call purge, demonstrated below,
  esmock.purge(loadConfig)
})

Calling esmock, typescript

esmock can be chained with ts-node/esm or tsm to mock typescript files. See the examples here. Situations that use sourcemaps and transpiled sources may require tests to specify a parentUrl, usually import.meta.url, to define the relative location from which mocked modules are located.

The parentUrl can be defined as the second param, and esmock then uses a different call signature where third and fourth params define child and global mocks,

await esmock(
  './to/module.js', // filepath to the source module being tested
  import.meta.url, // parentUrl from which mocked modules should be resolved
  { ...childmocks }, // mock definitions imported by the source module, directly
  { ...globalmocks }) // mock definitions imported anywhere in the source import tree

Calling esmock with a parentUrl as the second parameter,

test('should locate modules from parentUrl, transpiled location', async () => {
  const indexModule = await esmock('../index.js', import.meta.url, {
    os: { hostname: () => 'locale' }
  });
  
  assert.strictEqual(indexModule.getHostname(), 'local')
})

Calling esmock with a fourth options parameter, 'parent', is another way

test('should locate modules from parentUrl, transpiled location', async () => {
  const indexModule = await esmock('../index.js', {
    os: { hostname: () => 'locale' }
  }, null, { parent: import.meta.url });
  
  assert.strictEqual(indexModule.getHostname(), 'local')
})

Calling esmock, jest

jest-light-runner must be used to enable --loader with jest, as in the example test here. Set jest-light-runner as the jest runner and use NODE_OPTIONS like one of these,

  • NODE_OPTIONS=--loader=esmock jest,
  • NODE_OPTIONS="--loader=esmock --experimental-vm-modules" jest --config jest.config.cjs
  • examples, using jest

Calling esmock, absent module

Use the option param isModuleNotFoundError: false to mock modules not installed,

test('should mock vue package, even when package is not installed', async () => {
  const component = await esmock('../local/notinstalledVueComponent.js', {
    vue: {
      h: (...args) => args
    }
  }, {}, {
    isModuleNotFoundError: false
  })

  assert.strictEqual(component()[0], 'svg')
})

Problems or Edge Cases

Sometimes, when a module uses a complex export definition, esmock must return { default } rather than default. This is normal and sometimes necessary. Consider these different definitions returned by esmock,

const module1 = await esmock('./module1.js')
const { default: module2 } = await esmock('./module2.js')
const module3 = (await esmock('./module3.js')).default

Similarly, some mock definitions must define default explicitly as { default }. Consider these mock definitions,

const functionWithProperty = () => {}
functionWithProperty.namedValue = 'mock123'

const mocked = await esmock('./mymodule.js', {
  module1: () => 'the exported default value',
  module2: { default: () => 'the exported default value' },
  module3: { namedValue: 'mock123' },
  module4: { default: { namedValue: 'mock123' } },
  module5: { default: functionWithProperty }
})

Credit

Credit to Gil Tayar for showing a way to mock modules in his blog post here.

Clone this wiki locally