babel-plugin-mockable-imports
Advanced tools
Comparing version 1.4.0 to 1.5.0
@@ -8,2 +8,8 @@ # Changelog | ||
## [1.5.0] - 2019-05-13 | ||
- Support passing a function to `$mock` to semi-automatically mock imports | ||
based on the source, symbol name or original value | ||
[(#14)](https://github.com/robertknight/babel-plugin-mockable-imports/pull/14) | ||
## [1.4.0] - 2019-05-08 | ||
@@ -10,0 +16,0 @@ |
137
index.js
@@ -1,6 +0,6 @@ | ||
'use strict'; | ||
"use strict"; | ||
const pathModule = require('path'); | ||
const pathModule = require("path"); | ||
const packageName = require('./package.json').name; | ||
const packageName = require("./package.json").name; | ||
const helperImportPath = `${packageName}/lib/helpers`; | ||
@@ -16,3 +16,3 @@ | ||
// here breaks the plugin. | ||
'proxyquire', | ||
"proxyquire" | ||
]; | ||
@@ -27,5 +27,5 @@ | ||
*/ | ||
const EXCLUDED_DIRS = ['test', '__tests__']; | ||
const EXCLUDED_DIRS = ["test", "__tests__"]; | ||
module.exports = ({types: t}) => { | ||
module.exports = ({ types: t }) => { | ||
/** | ||
@@ -38,3 +38,3 @@ * Return the required module from a CallExpression, if it is a `require(...)` | ||
if ( | ||
node.callee.name === 'require' && | ||
node.callee.name === "require" && | ||
args.length === 1 && | ||
@@ -54,3 +54,3 @@ t.isStringLiteral(args[0]) | ||
return t.callExpression( | ||
t.memberExpression(t.identifier('$imports'), t.identifier('$add')), | ||
t.memberExpression(t.identifier("$imports"), t.identifier("$add")), | ||
[ | ||
@@ -60,4 +60,4 @@ t.stringLiteral(alias), | ||
t.stringLiteral(symbol), | ||
value, | ||
], | ||
value | ||
] | ||
); | ||
@@ -99,3 +99,3 @@ } | ||
return ( | ||
target.object.name === 'module' && target.property.name === 'exports' | ||
target.object.name === "module" && target.property.name === "exports" | ||
); | ||
@@ -118,3 +118,3 @@ } | ||
const collectCommonJSImports = { | ||
VariableDeclarator(path, {excludeImportsFromModules, imports}) { | ||
VariableDeclarator(path, { excludeImportsFromModules, imports }) { | ||
// If the `require` is wrapped in some way, we just ignore it, since | ||
@@ -143,4 +143,4 @@ // we cannot determine which symbols are being required without knowing | ||
if (id.type === 'Identifier') { | ||
if (id.name.startsWith('_')) { | ||
if (id.type === "Identifier") { | ||
if (id.name.startsWith("_")) { | ||
// Assume that imports with an underscore-prefixed name have been | ||
@@ -151,3 +151,3 @@ // automatically generated by Babel. Do not make them mockable. | ||
const symbol = '<CJS>'; | ||
const symbol = "<CJS>"; | ||
imports.push({ | ||
@@ -157,5 +157,5 @@ alias: id.name, | ||
source, | ||
value: id, | ||
value: id | ||
}); | ||
} else if (id.type === 'ObjectPattern') { | ||
} else if (id.type === "ObjectPattern") { | ||
// `var { aSymbol: localName } = require("a-module")` | ||
@@ -173,7 +173,7 @@ for (let property of id.properties) { | ||
symbol, | ||
value, | ||
value | ||
}); | ||
} | ||
} | ||
}, | ||
} | ||
}; | ||
@@ -210,14 +210,14 @@ | ||
t.importSpecifier( | ||
t.identifier('ImportMap'), | ||
t.identifier('ImportMap'), | ||
), | ||
t.identifier("ImportMap"), | ||
t.identifier("ImportMap") | ||
) | ||
], | ||
t.stringLiteral(helperImportPath), | ||
t.stringLiteral(helperImportPath) | ||
); | ||
const $importsDecl = t.variableDeclaration('const', [ | ||
const $importsDecl = t.variableDeclaration("const", [ | ||
t.variableDeclarator( | ||
t.identifier('$imports'), | ||
t.newExpression(t.identifier('ImportMap'), []), | ||
), | ||
t.identifier("$imports"), | ||
t.newExpression(t.identifier("ImportMap"), []) | ||
) | ||
]); | ||
@@ -227,8 +227,8 @@ | ||
t.exportSpecifier( | ||
t.identifier('$imports'), | ||
t.identifier('$imports'), | ||
), | ||
t.identifier("$imports"), | ||
t.identifier("$imports") | ||
) | ||
]); | ||
const body = path.get('body'); | ||
const body = path.get("body"); | ||
@@ -250,17 +250,17 @@ // Insert `$imports` declaration below last import. | ||
t.memberExpression( | ||
t.identifier('module'), | ||
t.identifier('exports'), | ||
t.identifier("module"), | ||
t.identifier("exports") | ||
), | ||
t.identifier('$imports'), | ||
t.identifier("$imports") | ||
); | ||
const cjsExport = t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
"=", | ||
moduleExportsExpr, | ||
t.identifier('$imports'), | ||
), | ||
t.identifier("$imports") | ||
) | ||
); | ||
body[body.length - 1].insertAfter(cjsExport); | ||
} | ||
}, | ||
} | ||
}, | ||
@@ -275,3 +275,3 @@ | ||
// Skip assignments that are not at the top level. | ||
if (path.parentPath.parent.type !== 'Program') { | ||
if (path.parentPath.parent.type !== "Program") { | ||
return; | ||
@@ -291,3 +291,6 @@ } | ||
// the right side of the assignment other than the `require` call. | ||
if (!t.isIdentifier(path.node.left) || !t.isCallExpression(path.node.right)) { | ||
if ( | ||
!t.isIdentifier(path.node.left) || | ||
!t.isCallExpression(path.node.right) | ||
) { | ||
return; | ||
@@ -318,3 +321,3 @@ } | ||
path.insertAfter( | ||
createAddImportCall(ident.name, source, '<CJS>', ident) | ||
createAddImportCall(ident.name, source, "<CJS>", ident) | ||
); | ||
@@ -330,3 +333,3 @@ }, | ||
// Ignore non-top level CommonJS imports. | ||
if (path.parent.type !== 'Program') { | ||
if (path.parent.type !== "Program") { | ||
return; | ||
@@ -337,10 +340,10 @@ } | ||
const imports = []; | ||
const {excludeImportsFromModules} = state.opts; | ||
const { excludeImportsFromModules } = state.opts; | ||
path.traverse(collectCommonJSImports, { | ||
excludeImportsFromModules, | ||
imports, | ||
imports | ||
}); | ||
// Register all found imports. | ||
imports.forEach(({alias, source, symbol, value}) => { | ||
imports.forEach(({ alias, source, symbol, value }) => { | ||
state.importIdentifiers.add(value); | ||
@@ -358,3 +361,3 @@ path.insertAfter(createAddImportCall(alias, source, symbol, value)); | ||
path.node.specifiers.forEach(spec => { | ||
if (spec.local.name === '$imports') { | ||
if (spec.local.name === "$imports") { | ||
// Abort processing the file if it declares an import called | ||
@@ -368,11 +371,11 @@ // `$imports`. | ||
switch (spec.type) { | ||
case 'ImportDefaultSpecifier': | ||
case "ImportDefaultSpecifier": | ||
// import Foo from './foo' | ||
imported = 'default'; | ||
imported = "default"; | ||
break; | ||
case 'ImportNamespaceSpecifier': | ||
case "ImportNamespaceSpecifier": | ||
// import * as foo from './foo' | ||
imported = '*'; | ||
imported = "*"; | ||
break; | ||
case 'ImportSpecifier': | ||
case "ImportSpecifier": | ||
// import { foo } from './foo' | ||
@@ -382,3 +385,3 @@ imported = spec.imported.name; | ||
default: | ||
throw new Error('Unknown import specifier type: ' + spec.type); | ||
throw new Error("Unknown import specifier type: " + spec.type); | ||
} | ||
@@ -393,3 +396,3 @@ | ||
path.insertAfter( | ||
createAddImportCall(spec.local.name, source, imported, spec.local), | ||
createAddImportCall(spec.local.name, source, imported, spec.local) | ||
); | ||
@@ -421,4 +424,4 @@ }); | ||
t.isIdentifier(callee.object) && | ||
callee.object.name === '$imports' && | ||
callee.property.name === '$add' | ||
callee.object.name === "$imports" && | ||
callee.property.name === "$add" | ||
) { | ||
@@ -431,3 +434,3 @@ return; | ||
// Bailing out here means that such code will at least compile. | ||
if (child.parent.type === 'ExportSpecifier') { | ||
if (child.parent.type === "ExportSpecifier") { | ||
return; | ||
@@ -438,11 +441,11 @@ } | ||
if ( | ||
child.parent.type === 'JSXOpeningElement' || | ||
child.parent.type === 'JSXClosingElement' || | ||
child.parent.type === 'JSXMemberExpression' | ||
child.parent.type === "JSXOpeningElement" || | ||
child.parent.type === "JSXClosingElement" || | ||
child.parent.type === "JSXMemberExpression" | ||
) { | ||
child.replaceWith( | ||
t.jsxMemberExpression( | ||
t.jsxIdentifier('$imports'), | ||
t.jsxIdentifier(child.node.name), | ||
), | ||
t.jsxIdentifier("$imports"), | ||
t.jsxIdentifier(child.node.name) | ||
) | ||
); | ||
@@ -452,10 +455,10 @@ } else { | ||
t.memberExpression( | ||
t.identifier('$imports'), | ||
t.identifier(child.node.name), | ||
), | ||
t.identifier("$imports"), | ||
t.identifier(child.node.name) | ||
) | ||
); | ||
} | ||
}, | ||
}, | ||
} | ||
} | ||
}; | ||
}; |
@@ -77,5 +77,13 @@ "use strict"; | ||
* | ||
* @param {Object} imports - | ||
* Map of file path (as used in the module that imports the file) to | ||
* replacement content for that module. | ||
* @param {Object|Function} imports - | ||
* An object whose keys are file paths (as used by the module being | ||
* tested, *not* the test module) and values are objects mapping export | ||
* names to mock values. As a convenience, the value can also be a | ||
* function in which case it is treated as a mock for the module's | ||
* default export. | ||
* | ||
* Alternatively this can be a function which accepts | ||
* (source, symbol, value) arguments and returns either a mock for | ||
* that import or `null`/`undefined` to avoid mocking that import. | ||
* This second form is useful for mocking many imports at once. | ||
*/ | ||
@@ -87,2 +95,19 @@ ; | ||
if (typeof imports === "function") { | ||
var mocks = {}; | ||
Object.keys(this.$meta).forEach(function (alias) { | ||
var _this$$meta$alias = _this.$meta[alias], | ||
source = _this$$meta$alias[0], | ||
symbol = _this$$meta$alias[1], | ||
value = _this$$meta$alias[2]; | ||
var mock = imports(source, symbol, value); | ||
if (mock != null) { | ||
mocks[source] = mocks[source] || {}; | ||
mocks[source][symbol] = mock; | ||
} | ||
}); | ||
this.$mock(mocks); | ||
} | ||
Object.keys(imports).forEach(function (source) { | ||
@@ -92,3 +117,3 @@ var sourceImports = imports[source]; | ||
if (typeof esImports === 'function') { | ||
if (typeof esImports === "function") { | ||
esImports = { | ||
@@ -101,6 +126,6 @@ "default": esImports | ||
var namespaceAliases = Object.keys(_this.$meta).filter(function (alias) { | ||
var _this$$meta$alias = _this.$meta[alias], | ||
source_ = _this$$meta$alias[0], | ||
symbol_ = _this$$meta$alias[1]; | ||
return source_ === source && symbol_ === '*'; | ||
var _this$$meta$alias2 = _this.$meta[alias], | ||
source_ = _this$$meta$alias2[0], | ||
symbol_ = _this$$meta$alias2[1]; | ||
return source_ === source && symbol_ === "*"; | ||
}); | ||
@@ -112,6 +137,6 @@ namespaceAliases.forEach(function (alias) { | ||
var cjsAliases = Object.keys(_this.$meta).filter(function (alias) { | ||
var _this$$meta$alias2 = _this.$meta[alias], | ||
source_ = _this$$meta$alias2[0], | ||
symbol_ = _this$$meta$alias2[1]; | ||
return source_ === source && symbol_ === '<CJS>'; | ||
var _this$$meta$alias3 = _this.$meta[alias], | ||
source_ = _this$$meta$alias3[0], | ||
symbol_ = _this$$meta$alias3[1]; | ||
return source_ === source && symbol_ === "<CJS>"; | ||
}); | ||
@@ -125,5 +150,5 @@ cjsAliases.forEach(function (alias) { | ||
var aliases = Object.keys(_this.$meta).filter(function (alias) { | ||
var _this$$meta$alias3 = _this.$meta[alias], | ||
source_ = _this$$meta$alias3[0], | ||
symbol_ = _this$$meta$alias3[1]; | ||
var _this$$meta$alias4 = _this.$meta[alias], | ||
source_ = _this$$meta$alias4[0], | ||
symbol_ = _this$$meta$alias4[1]; | ||
return source_ === source && symbol_ === symbol; | ||
@@ -130,0 +155,0 @@ }); |
{ | ||
"name": "babel-plugin-mockable-imports", | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"description": "Babel plugin for mocking ES imports", | ||
"main": "index.js", | ||
"scripts": { | ||
"checkformatting": "prettier --check **/*.js", | ||
"checkformatting": "prettier --check *.js test/*.js", | ||
"build": "babel helpers.js --out-dir lib", | ||
"lint": "eslint --ignore-pattern lib/* .", | ||
"format": "prettier --write **/*.js", | ||
"format": "prettier --write *.js test/*.js", | ||
"test": "mocha && npm run lint && npm run checkformatting", | ||
@@ -12,0 +12,0 @@ "prepublishOnly": "npm run build" |
@@ -5,11 +5,21 @@ # babel-plugin-mockable-imports | ||
A Babel plugin that modifies modules to enable mocking of their dependencies. The plugin was written with the following goals: | ||
A Babel plugin that transforms JavaScript modules (CommonJS or ES2015 style) | ||
to enable mocking of their dependencies in tests. | ||
- Provide a simple interface for mocking ES and CommonJS (`require`) module imports | ||
- Work when running tests in Node with any test runner or in the browser when | ||
using any bundler (Browserify, Webpack etc.) or no bunder at all | ||
- Minimize the amount of extra code added to modules, since extra code | ||
impacts test execution time | ||
- Catch common mistakes and warn about them | ||
See the Usage section below for information on getting started, and the FAQ at | ||
the end of the README for a comparison to alternative solutions. | ||
## Features | ||
- Provides a simple interface for mocking imports in tests | ||
- Works with CommonJS (Node style) and native (ES2015) JavaScript modules | ||
- Can be used with any test runner, any bundler, and whether tests are being run | ||
under Node or in the browser | ||
- Transforms code in a straightforward way that is easy to debug if necessary | ||
- Minimizes the amount of extra code added to modules, to reduce the impact | ||
on test execution time | ||
- Detects incorrect usage (eg. mocking a module or import that is not used) | ||
and causes a test failure if this happens | ||
- Can be used with both JavaScript and TypeScript | ||
## Usage | ||
@@ -108,2 +118,5 @@ | ||
See the [example project](examples/javascript) for a complete runnable project | ||
using Mocha as a test runner. | ||
### Mocking default exports | ||
@@ -143,2 +156,42 @@ | ||
### Mocking all imports that match a pattern | ||
In some tests you may want to mock many dependencies in the same way, or ensure | ||
that all imports meeting certain criteria in a module are mocked consistently. | ||
You can pass a function to `$imports.$mock` which will be called with the | ||
source, symbol name and original value of each import. The result of the | ||
function will be used as the mock for that import if it is not `null`. | ||
For example, to mock all functions imported by a module, you can use: | ||
```js | ||
$imports.$mock((source, symbol, value) => { | ||
if (typeof value === 'function') { | ||
// Mock functions using Sinon. | ||
return sinon.stub(); | ||
} else { | ||
// Skip mocking objects, constants etc. | ||
return null; | ||
} | ||
}); | ||
``` | ||
To ensure that a test mocks every imported function, you can use: | ||
```js | ||
// Throw an error if any unmocked function is called. | ||
$imports.$mock((source, symbol, value) => { | ||
if (typeof value === 'function') { | ||
return () => throw new Error('Function not mocked'); | ||
} | ||
return null; | ||
}); | ||
// Setup mocks for expected imports. | ||
$imports.$mock({ | ||
'./util': { doSomething: fakeDoSomething }, | ||
}); | ||
``` | ||
### Limiting mocking to specific files | ||
@@ -189,2 +242,12 @@ | ||
## Usage with TypeScript | ||
It is possible to use this plugin with TypeScript. In order to do that you need | ||
to transform your TypeScript code using Babel when running tests, and also | ||
use a helper function to get access to the `$imports` object for a module. | ||
Since this object is not present in the original source, the TypeScript compiler | ||
is not aware of its existence. | ||
See the [typescript example project](examples/typescript) for a runnable example. | ||
## How it works | ||
@@ -191,0 +254,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
37257
552
351