UNPKG

mock-import

Version:

Mocking of Node.js EcmaScript modules

241 lines (162 loc) 6.28 kB
# Mock Import [![License][LicenseIMGURL]][LicenseURL] [![NPM version][NPMIMGURL]][NPMURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Coverage Status][CoverageIMGURL]][CoverageURL] [NPMIMGURL]: https://img.shields.io/npm/v/mock-import.svg?style=flat [BuildStatusURL]: https://github.com/coderaiser/mock-import/actions?query=workflow%3A%22Node+CI%22 "Build Status" [BuildStatusIMGURL]: https://github.com/coderaiser/mock-import/workflows/Node%20CI/badge.svg [LicenseIMGURL]: https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat [NPMURL]: https://npmjs.org/package/mock-import "npm" [LicenseURL]: https://tldrlegal.com/license/mit-license "MIT License" [CoverageURL]: https://coveralls.io/github/coderaiser/mock-import?branch=master [CoverageIMGURL]: https://coveralls.io/repos/coderaiser/mock-import/badge.svg?branch=master&service=github Mocking of Node.js [EcmaScript Modules](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules), similar to [mock-require](https://github.com/boblauer/mock-require). ![Recording 2022-06-16 at 19 41 04](https://user-images.githubusercontent.com/1573141/174126105-58ba2664-d79f-4e1e-8cec-a8e84eb2bc2e.gif) ## Install ```sh npm i mock-import -D ``` ## Run [Loaders](https://nodejs.org/api/esm.html#esm_loaders) used to get things working, so you need to run tests with: ```sh node --import mock-import/register test.js ``` `mock-import` uses [transformSource hook](https://nodejs.org/api/esm.html#esm_transformsource_source_context_defaulttransformsource), which replaces on the fly all imports with constants declaration: ```js const {readFile} = global.__mockImportCache.get('fs/promises'); ``` `mockImport` adds new entry into `Map`, `stopAll` clears all mocks and `reImport` imports file again with new mocks applied. ## Supported Declarations ```js /* ✅ */ import fs from 'node:fs/promises'; /* ✅ */ import {readFile} from 'node:fs/promises'; /* ✅ */ import * as fs1 from 'node:fs/promises'; /* ✅ */ const {writeFile} = await import('fs/promses'); /* ✅ */ export * as fs2 from 'fs/promises'; /* ✅ */ export {readFile as readFile1} from 'fs/promises'; ``` ## Unsupported Declarations ```js /* ❌ */ export * from 'fs/promises'; // doesn't have syntax equivalent ``` ## How `mock-import` works? As was said before, [loaders](https://nodejs.org/api/esm.html#esm_loaders) used to get things working. This is `experimental` technology, but most likely it wan't change. If it will `mock-import` will be adapted according to `node.js API`. - `loader hook` intercepts into `import` process and get `pathname` of imported file; - if `pathname` in `reImports` it is processed with 🐊[**Putout**](https://github.com/coderaiser/putout) code transformer, changes all `import` calls to access to `__mockImportsCache` which is a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) filled with data set by `mockImport` call. And appends `sourcemap` at the end, so `node` can generate correct code `coverage`. ```diff -import glob from 'glob'; +const glob = global.__mockImportCache.get('./glob.js'); ``` - if `traceCache` contains `pathname` it calls are traced with [estrace](https://github.com/coderaiser/estrace); Code like this ```js const f = () => {}; ``` will be changed to ```js const f = () => { try { __estrace.enter('<anonymous:1>', 'trace.js:1', arguments); } finally { __estrace.exit('<anonymous:1>', 'trace.js:1'); } }; ``` Straight after loading and passed to `traceImport` stack will be filled with data this way: ```js __estrace.enter = (name, url, args) => stack.push([name, url, Array.from(args)]); ``` And when the work is done `stack` will contain all function calls. - `traceCache` contains some `paths` current file will be checked for traced imports and change them to form `${path}?count=${count}` to `re-import` them; ## Environment variables `mock-import` supports a couple env variables that extend functionality: - `MOCK_IMPORT_NESTED` - transform each `import` statement so mock of module work in nested imports as well (slowdown tests a bit) ## API ### `mockImport(name, mock)` - `name: string` - module name; - `mock: object` - mock data; Mock `import` of a `module`. ### `stopAll()` Stop all mocks. ### `reImport(name)` - `name: string` - name of a module Fresh `import` of a module. ### `traceImport(name, {stack})` - `name: string` name of a module - `stack: [fn, url, args]`; Add tracing of a module. ### `reTrace(name)` - `name: string` - name of traced module Apply tracing. ### `enableNestedImports()` Enable nested imports, can slowdown tests; ### `disableNestedImports()` Disable nested imports, use when you do not need nested imports support; ## Example Let's suppose you have `cat.js`: ```js import {readFile} from 'node:fs/promises'; export default async function cat() { const readme = await readFile('./README.md', 'utf8'); return readme; } ``` You can test it with 📼[`Supertape`](https://github.com/coderaiser/supertape): ```js import {createMockImport} from 'mock-import'; import {test, stub} from 'supertape'; const { mockImport, reImport, stopAll, } = createMockImport(import.meta.url); test('cat: should call readFile', async (t) => { const readFile = stub(); mockImport('fs/promises', { readFile, }); const cat = await reImport('./cat.js'); await cat(); stopAll(); t.calledWith(readFile, ['./README.md', 'utf8']); t.end(); }); ``` Now let's trace it: ```js import {createMockImport} from 'mock-import'; import {test, stub} from 'supertape'; const { mockImport, reImport, stopAll, } = createMockImport(import.meta.url); test('cat: should call readFile', async (t) => { const stack = []; traceImport('fs/promises', { stack, }); const cat = await reImport('./cat.js'); await cat(); stopAll(); const expected = [ ['parse', 'parser.js:3', [ 'const a = 5', ]], ['tokenize', 'tokenizer.js:1', [ 'parser call', 'const a = 5', ]], ]; t.deepEqual(stack, expected); t.end(); }); ``` ## License MIT