Running Mocha Tests as Native ES6 Modules in a Browser

Top modern browsers already support ES6 modules. This is great news from the unit testing perspective. A browser can natively load and test project sources without transpiler.

As a developer I would love to utilize this feature! Removing extra steps from the development process will improve speed and productivity.

Below is a story of how I got it working with Mocha test-runner.

Start with the official template

The official Mocha documentation contains the HTML template for running tests in the browser.

Let’s have a look at it:

The template uses traditional <script> tags without type="module" attribute. That means it does not utilize ES6 modules now.

The HTML code needs some adjustments to be more clear and up to date:

The template after cleanup:

Add a simple test to ensure it works

As a showcase I’ve created two files:

  1. The source file sum.js with a simple sum() function:

2. The test file sum.test.js checking that sum() is working well:

I’ve included these files into HTML with regular <script> tags:

...
<script>mocha.setup('bdd')</script>
<script src="sum.js"></script>
<script src="sum.test.js"></script>
<script>
mocha.checkLeaks();
mocha.run();
</script>
...

And opened the page in a browser:

Everything works. 🎉

Now let’s achieve the same result with native ES6 modules!

Switch to ES Modules

Two main syntax parts of ES modules are import and export. These statements allow to set dependencies between modules.

  1. The source file sum.js turns into a module that exports function:

2. The test file sum.test.js turns into a module that imports function and performs test:

Because test file now imports source file, the HTML page can include only test file, not both files as before. To import ES module in the HTML code a <script> tag should have type="module" attribute:

...
<script>mocha.setup('bdd')</script>
<script type="module" src="sum.test.js"></script>
<script>
mocha.checkLeaks();
mocha.run();
</script>
...

Let’s try to open the page in a browser. The result is blank screen :( This is because ES modules are deferred by default. The call of mocha.run() in the code snippet above occurs before sum.test.js is loaded!

The workaround is to keep script execution order the same as <script> tags appear in a document. With non-inline scripts defer attribute can do the job. But here is inline script and defer attribute is not applicable.

The solution is to set type="module" to inline script either. Although it does not import/export anything, the browser preserves execution order and calls mocha.run() after the test is loaded:

...
<script type="module" src="sum.test.js"></script>
<script type="module">
mocha.checkLeaks();
mocha.run();
</script>
...

Now it works! Both files are loaded as ES modules and the test is passing:

This is an excellent result for the development experience! I can edit ES module files and re-run tests in the browser without transpiling!

For checking let’s make a bug. 🐜

The test shows error after page reload:

Improve the result

The solution works but can be improved. Currently the test-running logic is mixed with HTML markup as inline scripts. I prefer to keep all logic out of HTML. The markup should be clear and simple.

A pretty straightforward way is to put everything in a single JS module:

..and include only this file into the page:

...
<body>
<div id="mocha"></div>
<script type="module" src="run.js"></script>
</body>
...

Now, the HTML looks very clear! But it does not work.

There are two problems:

  1. The first problem is related to Mocha itself. It is impossible to import current version of Mocha (4.0.1) as ES module. I’ve tested different approaches but with no success:
import 'https://unpkg.com/mocha@4.0.1/mocha.js';
// Uncaught ReferenceError: require is not defined
import 'https://unpkg.com/mocha@4.0.1/index.js';
// Uncaught ReferenceError: module is not defined
import mocha from 'https://unpkg.com/mocha@4.0.1/mocha.js';
// Uncaught SyntaxError: The requested module does not provide an export named 'default'

Finally I’ve returned Mocha loading to the regular <script> tag and opened issue in the Mocha repository.

2. The second problem is related to the static nature of ES6 modules. All import statements in a module are resolved before running the code. But Mocha tests rely on global describe() / it() functions that must exist at the time of test execution. Importing test-file as a module throws error because Mocha did not export global functions yet:

mocha.setup('bdd');     // <-- exports global describe() / it()
import './sum.test.js'; // <-- executed before mocha.setup('bdd')

The only solution here is to have two separate ES modules loaded one after another. The first one is to setup testing and assertion globals: describe() / it() and chai. The second one is to load tests and start runner.

Setup:

Run:

HTML:

Now the goal is achieved! HTML is pretty simple and logic lives in JS files. I can scale this solution by adding more test-files into run.js.

Fallback for older browsers

The last interesting challenge is to make this page working in older browsers.

They do not understand <script type="module"> and do not execute ES modules. For these browsers all imports should be resolved statically, bundled into a single file and included on the page as <script nomodule>. The nomodule attribute prevents loading of fallback script in the modern browsers that know about ES modules.

To pack existing setup.js and run.js modules into a single file I‘ll use webpack. It can resolve import / export statements out of box.

Let’s start with the simple webpack config:

But the build failed:

> webpack
ERROR in ./setup.js Module not found: 
Can't resolve 'https://unpkg.com/chai@4.1.2/chai.js'

This is because setup.js tries to import remote url:

import 'https://unpkg.com/chai@4.1.2/chai.js';

Webpack is intended to work with local filesystem. But it has aliases mechanism that allows to map remote and local resources. I’ve installed chai.js locally from npm and mapped remote chai.js url to node_modules:

Now build passes and outputted bundle.js can be included into the HTML:

To check the result I’ll use Firefox 57. The support of ES modules is under the flag there and disabled by default. The first attempt displays an error:

The chai assertion object loaded from node_modules/chai haven’t been added to window. Why? Internally chai is built as UMD library. That means it uses window only if there is no special variables like define, module and exports. Webpack imported chai as a CommonJS module and provided exports variable. That’s the reason!

How to tell webpack to treat file as a ready to use script and do not provide special variables? The documentation has an answer:

The script-loader evaluates code in the global context, similar to inclusion via a script tag. In this mode, every normal library should work. require, module, etc. are undefined.

This is exactly what is needed. Adding script-loader to the webpack config fixed the error. chai.js is loaded and available globally. The final webpack config is:

The test passes in Firefox!

Now the testing page works in all browsers:

  • If browser supports ES modules — the scripts are loaded as ES modules.
  • If browser does not support ES modules — the page fallbacks to bundled script.
You can check it online in your browser. The source code is available on GitHub.

Conclusion

ES modules is a way all future JavaScript apps will be arranged. Easy testing of such apps is an important point for test-runners along with performance, snapshot testing and other features.

The example above is a starting setup for Mocha. It can be extended further by running in a Headless Chrome, integrating with Karma or applying to another test-runner. But that’s a topic for another day. 🙂

Thanks for reading and happy testing!