Using ES6 (and even far future versions like ES7!) is becoming very easy these days – just set up Babel, and you’re off to the races. If you’re only writing code for NodeJS, you might even get away without Babel, as the native ES6 support is getting very good.
The workflows are easy and detailed for development, but what about testing? How do you write unit tests for ES6 code? How do you even configure the testing tools to work with the new features?
In this article, I’ll show you how to configure the most popular testing tools – Mocha, Jasmine, Karma, and Testem – to work with ES6. We’ll also go over some best practices for testing ES6 code and writing tests with ES6. You won’t have to neglect testing your ES6 code anymore!
Prerequisites
Before we start, we need to install some necessary tools:
- We need Babel and some related libraries to compile ES6 code
- And we need Webpack or Browserify to bundle our modules together
Even if your project has these set up already, you should check the sections below. Some of the testing tools may require additional libraries which you’ll otherwise miss.
Installing Babel and its libraries
Regardless of which testing tool or bundler you use, we need Babel and babel-polyfill. As you might know, Babel itself converts the new ES6 syntax to something older JavaScript engines understand, and babel-polyfill adds the missing objects, such as Promise
, and new functions, like Array.prototype.find
.
npm install --save babel babel-polyfill
If you intend to test in Node.js, you’ll also need babel-register. This module allows you to automatically compile code with Babel with your testing tool of choice.
npm install --save babel-register
You may also need to install your Babel presets of choice, such as es2015 and react.
npm install --save babel-preset-es2015 babel-preset-react
Configuring Babel
Although you can pass Babel configuration options on the command-line to many tools, or put them into your package.json
file, I recommend using a .babelrc
file instead.
Babel automatically picks up settings from .babelrc
. This happens even if you use a tool, which then calls Babel for you. Putting your configuration options into .babelrc
means you don’t need to maintain the settings in multiple locations.
Here’s an example .babelrc
file when using the es2015 and react presets:
{
"presets": ["es2015", "react"]
}
Setting up Webpack or Browserify
NodeJS user note: If you only intend to test code with Node, you don’t need to do this step. You can skip straight to the section discussing your testing tool of choice.
Browserify users should install the babelify transform library. This allows Browserify to transform the code using Babel during the bundling process.
npm install --save babelify
You can either pass it as a parameter for browserify, using…
browserify -t [ babelify ] some-file.js -o some-output-file.js
…or set it up in package.json:
"browserify": {
"transform": ["babelify"]
}
For webpack, you need to install babel-loader, which is used by Webpack to compile code with Babel.
npm install --save babel-loader
Then, set it up in your Webpack configuration file, for example:
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel'
}
]
}
The above configuration will compile .js
and .jsx
files with Babel, except the files in node_modules
. Excluding the module directory can speed up the compilation process significantly.
Setting up the testing tools
With the necessary prerequisites set up, we can now move on to setting up the testing tools themselves.
Below, you’ll find a section detailing how to configure each tool, and after that, we’ll tackle writing tests.
Mocha
With Node.js all you need is to run Mocha with the correct parameters:
mocha --compilers js:babel-register --require babel-polyfill
This enables the Babel compiler for JavaScript files and automatically requires the babel-polyfill module.
Because this is a pretty long command to type by hand, you can set this up for example to your npm scripts in package.json:
"scripts": {
"test": "mocha --compilers js:babel-register --require babel-polyfill"
}
Note that Mocha will automatically load tests from test/
. If you want to put your tests to some other directory, you need to specify the directory:
mocha --compilers js:babel-register --require babel-polyfill --recursive path/to/tests
We use the --recursive
flag above the ensure the tests are loaded from path/to/tests
even if they are within a subdirectory.
For browsers, you need to use Webpack or Browserify to compile all test files.
- Browserify:
browserify test/**/*.js -o tests-bundle.js
- Webpack:
webpack test/**/*.js tests-bundle.js
The above commands would bundle all JS files from test/
into a file called test-bundle.js
.
Then, in your test-runner HTML file, simply include tests-bundle.js
<title>Mocha Tests</title>
<link rel="stylesheet" href="node_modules/mocha/mocha.css">
<div id="mocha"></div>
<script src="node_modules/mocha/mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script src="tests-bundle.js"></script>
<script>
mocha.run();
</script>
The order of loading files here doesn’t matter, as long as you include the bundled file before calling mocha.run()
. If you use require
within your tests to load any assertion libraries or other tools you’re using, you don’t need to include their scripts in the runner either.
Jasmine
For Node.js, Jasmine is unfortunately not an ideal choice. Although it works, setting up Babel for it is a bit hackier than for Mocha.
Unlike Mocha, Jasmine doesn’t give us a command-line flag for compilation. Therefore, we need to run Jasmine using babel-node
. The babel-node tool is a wrapper around node
, which runs your code through Babel.
To make running Jasmine with it easier, we’ll also install Jasmine to our local node_modules directory.
npm install -g babel-cli
npm install jasmine
In order to make Jasmine work, you need to first initialize its configuration file using…
node_modules/.bin/jasmine init
This creates the file spec/support/jasmine.json
which you can modify to configure things such as the location of your test files.
We can then run our Jasmine specs with Babel using…
babel-node node_modules/.bin/jasmine
It is a bit of a mouthful, and as usual, you can set it up as an npm script among other options.
"scripts": {
"test": "babel-node node_modules/.bin/jasmine"
}
When testing in browser, Jasmine’s steps are the same as for Mocha. Use your favorite module bundler to generate the ES6 output for your tests, and include the tests in the spec runner file.
- Browserify:
browserify test/**/*.js -o tests-bundle.js
- Webpack:
webpack test/**/*.js tests-bundle.js
Karma
With Karma, to run Babelified tests in browsers, we need to include the karma-babel preprocessor, which allows Karma to compile things with Babel.
npm install karma-babel-preprocessor
Once installed, you can add the following configuration options to your Karma configuration file:
preprocessors: {
"src/**/*.js": ["babel"],
"test/**/*.js": ["babel"]
}
This enables Babel compilation for any .js
file within the directories src/
and test/
Testem
Testem is easy to set up for ES6. You only need to add the following configuration options to testem.json:
"serve_files": ["tests-bundle.js"],
"before_tests": "browserify -t [ babelify ] test/**/*.js -o tests-bundle.js"
The serve_files
option tells testem about any additional files it should serve to the browser for testing. Since we use the before_tests
option to bundle our code into the file tests-bundle.js
, we include it with serve_files
Alternatively, using webpack…
"serve_files": ["tests-bundle.js"],
"before_tests": "webpack test/**/*.js tests-bundle.js"
Writing unit tests for ES6 code
Now with the tools set up, let’s take a look at how we would approach writing tests for ES6 code. The examples below use Mocha and Chai, but you can apply the same principles with Jasmine as well.
The basics
The basics are the same as when testing non-ES6 code. We use describe
and it
to set up our test case, but it’s now possible to use ES6 features to improve our code.
Create a directory called test/
and place the following in a file called test/arithmeticTest.js
within it.
const chai = require('chai').expect;
describe('Arithmetic', () => {
it('should calculate 1 + 1 correctly', () => {
const expectedResult = 2;
expect(1 + 1).to.equal(expectedResult);
});
});
The above snippet demonstrates ES6 features in tests. We’re using const
instead of var
when loading Chai. This means we can’t accidentally re-define things, and it also communicates our intention that it should never change.
We also make use of arrow functions. You can slightly simplify the code with them, however, they do come with some possible issues which we’ll look at in the best practices part of this article.
And lastly, similar to loading Chai, we use const
to declare the expected result variable. This can again avoid problems, and it clarifies the intention of the value never changing.
We can run the above test using Mocha, with the command shown earlier in the article:
mocha --compilers js:babel-register --require babel-polyfill
Async tests
Writing asynchronous tests with arrow functions works the same way as before, by passing the done
callback:
describe('Timeout', () => {
it('should call the callback', (done) => {
setTimeout(() => done(), 500);
});
});
Mocha also supports promises out of the box, which makes writing asynchronous tests very easy. We’ll look at the below in the best practices section.
ES6 imports
It’s also possible to use ES6 imports within your tests. Remember: test code is just code. Anything that would work in your app code also goes in your test code, now that we’ve set up the tools!
import { expect } from 'chai';
describe('Arithmetic', () => {
it('should calculate 1 + 1 correctly', () => {
const expectedResult = 2;
expect(1 + 1).to.equal(expectedResult);
});
});
This works exactly the same as using require.
Best Practices
There are some potential ES6-specific best practices and gotchas you may run into, which we’ll look at below.
Take care with arrow functions in Mocha
Be careful with arrow functions and Mocha. In some cases, you may need to use this.timeout
for example to control how long a test will wait before timing out. Using arrow functions, it won’t work.
The reason for this is how arrow functions work with this
. As a result, Mocha can’t bind its helper functionality correctly. If you don’t need to use any of the helpers, using arrow functions is OK.
Avoid arrow functions with Sinon
Similar to Mocha, Sinon.js can be problematic in some cases with arrow functions.
The problem is sinon.test
. It’s a good idea to use this when using test doubles within your tests, as it avoids problems with restoring the doubles at the end of the test. But, it uses this
bindings, and because of the special this
behavior with arrow functions, it doesn’t work correctly with arrow functions.
Either don’t use arrow functions when using sinon.test
, or initialize and restore a sandbox manually in beforeEach
and afterEach
.
var sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
afterEach(() => {
sandbox.restore();
});
it('should do something with a sandbox', () => {
//similar to sinon.test, this stub will be cleaned up automatically
var stub = sandbox.stub();
});
Mocha works out of the box with promises
With Mocha, testing code with ES6 Promises is a piece of cake. Mocha has support for promises built-in, so you can return a promise from a test, and Mocha handles it for you.
import { expect } from 'chai';
it('should succeed when promise is resolved', () => {
const result = Promise.resolve('success');
return result.then(function(value) {
expect(value).to.equal('success');
});
});
Normally you would need an asynchronous test when using promises. But since Mocha supports promises, we can return a promise from the test, and Mocha waits until it gets resolved.
Mocha is also smart enough to mark a test as failed if the promise is rejected, such as below:
it('this test always fails', () => {
return Promise.reject('error message');
});
For more details, check out my article Promises in JavaScript Unit Tests: The Definitive Guide.
How to test ES6 generators?
If you want to test code with generators, Mocha’s support for promises means we can use co.wrap
from the co module.
npm install co
Then, just wrap your test function with it:
it('should do something with generators', co.wrap(function*() {
var result = yield doSomeGeneratorThing();
}));
Make use of source maps
Debugging your test code may become more difficult when using bundlers. They stick all your code together, so it becomes hard to tell which file is causing problems.
To fix this, you can enable source maps when bundling the code.
- Browserify: to enable source maps with browserify, use the
-d
flag, such as…
browserify -d -t [ babelify ] test/**/*.js -o tests-bundle.js
- Webpack: you can enable source maps by including
devtool: 'source-map'
in your webpack configuration file
If you’re running your tests from the command line, source maps are unnecessary.
Conclusion
Testing ES6 code is easy; it just requires a little bit of configuration for the tooling. In the future with improved ES6 support, you can do away with these, unless you want to make use of Babel for some other purpose (such as supporting ES7).
Writing tests with ES6 is mostly the same as writing tests without it. Just keep in mind that arrow functions can be problematic, and you’re good.
So which tool should you use? I recommend Mocha. It has the best support for ES6 testing, thanks to its native support for promises. It also works nicely with the existing tools and libraries.