-
Notifications
You must be signed in to change notification settings - Fork 18
Home
+ ██╗
+ ██████╗ ███████╗ █████═████╗ ██████╗ ██████╗██║ ██╗
+██╔═══██╗██╔═════╝██╔══██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝
+████████║╚██████╗ ██║ ██║ ██║██║ ██║██║ ██████╔╝
+██╔═════╝ ╚════██╗██║ ██║ ██║██║ ██║██║ ██╔══██╗
+╚███████╗███████╔╝██║ ██║ ██║╚██████╔╝╚██████╗██║ ╚██╗
+ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝
esmock provides native ESM import mocking on a per-unit basis.
$ 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"
}
}
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'), '🌎')
})
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
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 merges original module definitions with mock definitions.
mockPartial = Object.assign(await import('/module.js'), mockDefs)
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 useesmock.strict(...args)
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
// esmock is unable to apply this short style for some modules
'../src/init.js': () => 'init'
}
This example mocks imported package and local file modules,
/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' }))
})
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 "partial mocking" behaviour merges the core "path" definition with the mocked "path" definition, to include "path.dirname". The "strict mocking" behaviour uses the mock definition with "basename" only and "dirname" is never defined, resulting in a runtime error.
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)
})
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')
})
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
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')
})
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 to Gil Tayar for showing a way to mock modules in his blog post here.