Keep Moving Forward | X-Team Magazine

Setting Up JavaScript Testing Tools for ES6

Written by Jani Hartikainen | May 23, 2016 4:00:00 AM

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:

  1. We need Babel and some related libraries to compile ES6 code
  2. 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.