3 Ways to Use ES6 Modules with Mocha
Latest update:
The problem: Mocha was written long before es6 modules became common
within browsers. It's a classic node program that internally uses
require()
fn to load each test file. Therefore, running mocha under
--experimental-modules
node flag does no good, for in the es6
modules mode there is no require()
fn.
Say we have 2 modules:
$ cat foo.mjs
export default function() { return 'foo' }
$ cat bar.mjs
export default function() { return 'bar' }
&
$ cat package.json
{
"devDependencies": {
"mocha": "5.2.0"
}
}
How can we test them using the celebrated Mocha that doesn't know
anything about es6 modules?
1. Babel 7
Cons |
Slow |
Depends on Babel with a 3rd party
plugin |
Cumbersome to setup |
This is the most famous method & the most sluggish one. It works like
this: Mocha instructs Babel to convert import
statements into
require()
calls, bind Babel to those calls through a special hook,
then compiles the rest of mandatory files on the fly. Though it caches
the compilation results (see node_modules/.cache
dir), it's
nevertheless noticeably slower than running the equivalent tests
written in commonjs style.
It you like your node_modules
directory fat, this option is for you!
We write tests for each module in a separate file. For foo
module we
use the usual static imports:
$ cat foo_test.mjs
import assert from 'assert'
import foo from './foo.mjs'
suite('Foo', function() {
test('smoke', function() {
assert.equal(foo(), 'foo')
})
})
but for bar
module we employ a dynamic import to make the test less
ordinary:
$ cat bar_test.mjs
import assert from 'assert'
suite('Bar', function() {
test('smoke', function() {
return import('./bar.mjs').then( module => {
let bar = module.default
assert.equal(bar(), 'bar')
})
})
})
Now we need to specify a proper list of dependencies, then write a
configuration for Babel. For simplicity's sake, we confine everything to
package.json
:
{
"devDependencies": {
"@babel/core": "7.1.0",
"@babel/preset-env": "7.1.0",
"@babel/register": "7.0.0",
"babel-plugin-dynamic-import-node": "2.1.0",
"mocha": "5.2.0"
},
"babel": {
"plugins": [
[
"dynamic-import-node"
]
],
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
This is an absolute minimum (which is a sad state of affairs). You may
omit babel-plugin-dynamic-import-node
if you don't use dynamic
imports (but we do here). Btw, don't be tempted to use the official
@babel/plugin-syntax-dynamic-import
plugin--it doesn't do any
transformations at all, for it's written in the main for tools like
Webpack that can handle the conversions in place of Babel.
Run the tests:
$ npm i
...
$ node_modules/.bin/mocha -R list --require @babel/register -u tdd *test.mjs
✓ Bar smoke: 4ms
✓ Foo smoke: 0ms
2 passing (21ms)
The size of the dependency tree is of course depressing:
$ rm -rf node_modules/.cache
$ du -h --max-depth=0 node_modules
16M node_modules
$ find node_modules -type f | wc -l
4870
$ find node_modules -name package.json | wc -l
161
2. In the browser
No compilation step | Chai dependency |
| Every test file must be listed explicitly |
The version of Mocha for the browser obviously doesn't contain any
require()
fn calls & doesn't know about any module systems.
Our package.json
looks much simpler:
{
"devDependencies": {
"mocha": "5.2.0",
"chai": "4.1.2"
}
}
Unfortunately we need to edit *_test.mjs
files, for neither the
browser doesn't have a built-in assert
module, nor there is a way to
inline the wrapper around Chai for mocking a global es6 module. Thus,
foo_test.js
becomes:
import foo from './foo.mjs'
suite('Foo', function() {
test('smoke', function() {
// assert is global & comes from Chai
assert.equal(foo(), 'foo')
})
})
(The same goes for bar_test.mjs
.)
The test runner is an html page:
<!doctype html>
<link rel="stylesheet" href="node_modules/mocha/mocha.css">
<script src="node_modules/chai/chai.js"></script>
<script src="node_modules/mocha/mocha.js"></script>
<div id="mocha"></div>
<script type="module">
window.assert = chai.assert
mocha.setup('tdd')
</script>
<script type="module" src="foo_test.mjs"></script>
<script type="module" src="bar_test.mjs"></script>
<script type="module">
mocha.run()
</script>
Nothing should be surprising here, except that the "setup" parts must
be marked as "modules" too.
3. Monkey Patching
No compilation step | A custom wrapper instead of the mocha executable |
0 dependencies | |
Mocha has an API. It expects a commonjs usage, but we can always
overwrite 2 methods in Mocha
class: loadFiles()
& run()
:
$ cat mocha.mjs
import path from 'path'
import fs from 'fs'
import Mocha from './node_modules/mocha/index.js'
Mocha.prototype.loadFiles = async function(fn) {
var self = this;
var suite = this.suite;
for await (let file of this.files) {
file = path.resolve(file);
suite.emit('pre-require', global, file, self);
suite.emit('require', await import(file), file, self);
suite.emit('post-require', global, file, self);
}
fn && fn();
}
Mocha.prototype.run = async function(fn) {
if (this.files.length) await this.loadFiles();
var suite = this.suite;
var options = this.options;
options.files = this.files;
var runner = new Mocha.Runner(suite, options.delay);
var reporter = new this._reporter(runner, options);
function done(failures) {
if (reporter.done) {
reporter.done(failures, fn);
} else {
fn && fn(failures);
}
}
runner.run(done);
};
let mocha = new Mocha({ui: 'tdd', reporter: 'list'})
process.argv.slice(2).forEach(mocha.addFile.bind(mocha))
mocha.run( failures => { process.exitCode = failures ? -1 : 0 })
Using node v10.10.0:
$ node --experimental-modules mocha.mjs *test.mjs
(node:100618) ExperimentalWarning: The ESM module loader is experimental.
✓ Bar smoke: 2ms
✓ Foo smoke: 0ms
2 passing (16ms)
The wrapper is intentionally bare bone. loadFiles()
fn here is very
similar to its original version, only it became async, for the es6
dynamic import statement returns a promise. run()
fn, on the other
hand, is significantly shorter then the original, for we don't support
any CLOs & assume any CL argument to be a file name.
You can add at least -g
option to the wrapper as a homework.
Tags: ойті
Authors: ag