-
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
Note: For versions of node prior to v20.6.0, "--loader" command line arguments must be used with esmock
as demonstrated below. Current versions of node do not require "--loader".
{
"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",
"test-tsx": "⚠ https://github.com/esbuild-kit/tsx/issues/264"
},
"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
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. For each mocked-module, the test context must define every name imported to the mock tree. When imported 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 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 = {
// package
eslint: { ESLint: cfg => cfg },
// core module
fs: { readFileSync: () => 'returns this globally' },
// global variable
import: { fetch: () => ({ res: 200 }) },
// local file
'../src/main.js': { start: 'go' },
// subpath
'#feature': { release: () => 'release time' },
// default and/or named exports
'../src/hello.js': {
default: () => 'world',
exportedFunction: () => 'foo'
},
// short-hand default exports
'../src/init.js': () => 'init'
}
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' }))
})
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.
Use esmock.strictest
or import { strictest } from 'esmock'
to use the strictest version of esmock that requires every module imported to the target module to be mocked.
const strictestTree = await esmock.strictest(
'../local/importsCoreLocalAndPackage.js', {
path: { basename: () => 'core' },
'../local/usesCoreModule.js': { readPath: () => 'local' },
'form-urlencoded': () => 'package'
}
)
assert.deepEqual(strictestTree.corePathBasename(), 'core')
assert.deepEqual(strictestTree.localReadSync(), 'local')
assert.deepEqual(strictestTree.packageFn(), 'package')
await assert.rejects(async () => esmock.strictest(
'../local/importsCoreLocalAndPackage.js', {
'../local/usesCoreModule.js': { readPath: () => 'local' },
'form-urlencoded': () => 'package'
}
), {
name: 'Error',
message: new RegExp(
'un-mocked moduleId: "node:path" \\(used by .*importsCoreLocalAndPackage.js\\)')
})
Add mock global definitions to an 'import' namespace. This example mocks the global variable 'fetch'. A new line is added to the top of the mocked file at runtime, which imports the mocked global definition like this; import { fetch } from 'file:///esmock.js'
req.js
export default async url => fetch(url + '?limit=10')
req.test.js
const mockedReq = await esmock('../src/req.js', {
import: {
fetch: () => '[["jim",1],["jen",2]]'
}
})
await mockedReq() // '[["jim",1],["jen",2]]'
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: () => 'local' }
});
assert.strictEqual(indexModule.getHostname(), 'local')
})
- examples, chaining loaders
- examples, defining parentUrl
- examples, using ts and jest, jest-light-runner required
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
- examples, using ts and jest, jest-light-runner required
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')
})
The esmock
resolver is unable to resolve modules in other workpaces. The below sample, which attempts to mock a module in another workspace, will not work,
import test from 'node:test'
import assert from 'node:assert/strict'
import esmock from 'esmock'
test('mock a module from another workspace', async () => {
const sut = await esmock('./moduleInCurrentWorkspace.js', {
moduleInOtherWorkspace: {
foo: () => 'bar'
}
})
assert.equal(sut(), 'bar')
})
To get around this limitation, module.createRequire
can be used as seen below. Credit to @tpluscode for describing this approach.
import test from 'node:test'
import assert from 'node:assert/strict'
import esmock from 'esmock'
import module from 'module'
const require = module.createRequire(import.meta.url)
test('mock a module from another workspace', async () => {
const sut = await esmock('./moduleInCurrentWorkspace.js', {
[require.resolve('moduleInOtherWorkspace')]: {
foo: () => 'bar'
}
})
assert.equal(sut(), 'bar')
})
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.