Build Tools for Test-driven Development with JavaScript

So many testing how-to blogs tell you how to write the tests or how to run the suite, or describe a bunch of tools, and end there. They make the assumption that you know how to set up the test runner to work with your application. If you get stuck at this point, then this blog should help you out. 

 
There are a few key questions we’ll try to answer here:

  1. How do you run your tests, instead of running your app?
  2. How do you set up your test config (with handy import shortcuts!)
  3. How do you write a main spec helper to make your life easier as testing the app grows more complex?

How do you run your tests, instead of running your app?

We are going to assume you’re using webpack, since it’s popular.  We’re also going to assume you’re using mocha, since that’s kind of what this is about.  (If you want to use some alternative tool, like jest, you might just want to take a look at the create-react-app package on npm, which is great and we can also recommend, but is not the subject of this series.)

Normally your app gets built from an index.js or an app.js that’s the root of the requirements for your site. Webpack defines this using its entrykey. We will use the mocha-webpack tool which overrides this entry point based on your configuration. This prevents the app itself from being run (since we are just testing) and hands over the require-tree responsibility to webpack-mocha. You can use your webpack config or our example webpack.config.js below. The test config we’ve provided will just add some options we need for testing.

 

If you haven’t already set up webpack as a build tool for your app, here’s how 

Install the builder (webpack) with npm:

$ npm install --save-dev webpack

For the webpack.config.js that will build your app, the following packages are needed:

$ npm install --save-dev copy-webpack-plugin html-webpack-plugin html-webpack-template

$ npm install --save babel-plugin-transform-react-jsx

We’d like to use es6 and JSX so we’re going to install babel (which is a “transpiler”) and it’s React/JSX plugin here as well. Transpiling allows us to use an alternative syntax and compose our code for compatibility with current web browsers or other targets.

Our transpiler, babel, can be configured to build your project separately, but we recommend you use the babel-loader for webpack, instead, to reduce steps.

Here’s what our webpack.config.js looks like

View webpackconfig.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');


module.exports = {
  entry: path.join(__dirname, 'src', 'app.js'),
  output: {
    path: __dirname + '/build',
    filename: 'app.js'
  },
  plugins: [
    //provides variables by default to all modules
    new webpack.ProvidePlugin({
      'React': 'react',
      'ReactDom': 'react-dom',
      'Redux': 'redux'
    }),

    // combined with 'html-webpack-template' below, creates a default html document.
    new HtmlWebpackPlugin({
      // Required
      inject: false,
      template: require('html-webpack-template'),
      //template: 'node_modules/html-webpack-template/index.ejs',

      // Optional
      appMountId: 'app'
      // and any other config options from html-webpack-plugin
      // https://github.com/ampedandwired/html-webpack-plugin#configuration
    }),
    new CopyWebpackPlugin([
      {
        context: path.join(__dirname, 'assets'),
        from: '**/*',
        to: path.join(__dirname, 'build', 'assets')
      }
    ])
  ],
  module: {
    loaders: [
      {
        test: /(\.scss|\.sass|\.css)$/,
        loaders: ["style", "css?sourceMap", "sass?sourceMap"]
      },

      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        query: {
          presets: ['es2015', 'react'],
          plugins: ['transform-react-jsx']
        }
      }
    ]
  },
  resolve: {
    root: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')]
  }
}

 

An inspection of this webpack config file (specifically, the plugin section) shows that these modules are made available to our app. Normally, to include JSX, React is a required import, since the JSX will be converted to React.createElement calls in the transpiled source. As a result of the plugin configuration, explicit requirements for React are not required and are included implicitly, by webpack.

Towards the bottom of both webpack-config files, there is a resolve key that is an object. This neat trick allows us to call for modules we have written without specifying the absolute path.  See example:

import mathObj  from ‘'../src/math'; // without the resolve key defining the base path

import mathObj from 'math'; // with the resolve key

How do you set up your test config?

Next, we install the testing compatibility tool (mocha-webpack).

 npm install --save-dev mocha-webpack

We’ll also add a handy shortcut to your app’s configuration. In the package.json file, the test script should look like:

"scripts": {
 "test": "NODE_ENV=test mocha-webpack --watch"
}

Our tool, mocha-webpack will use the file webpack.config-test.js to build the source for your main project.

Here’s what our webpack.config-test.js looks like

View webpack.config-test.js
var webpack = require('webpack');
var path = require('path');
var nodeExternals = require('webpack-node-externals');

var webpackConfig = require('./webpack-config');

module.exports = Object.assign({}, webpackConfig, {
  output: Object.assign({},  webpackConfig, {
    // sourcemap support for IntelliJ/Webstorm
    devtoolModuleFilenameTemplate: '[absolute-resource-path]',
    devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
  }),
  plugins: Array.from(webpackConfig.plugins).concat([
    //provides variables by default to all modules
    new webpack.ProvidePlugin({
      'deepFreeze': 'deep-freeze'
    }),
  ]),
  target: 'node', // in order to ignore built-in modules like path, fs, etc.
  externals: [nodeExternals()], // in order to ignore all modules in node_modules folder
  devtool: "cheap-module-source-map" // faster than 'source-map'
});

 

In addition to the builder configuration for testing, mocha-webpack.opts may contain several additional flags for configuration.We use mocha-webpack.opts to override command-line flags specified in the package.json and build files.

Here is our sample mocha-webpack.opts

View mocha-webpack.opts
--colors
--include spec/spec-helper.js
--reporter spec
--require source-map-support/register
--webpack-config webpack.config-test.js
spec/

 

Note the --include spec/spec-helper.jsline, that includes our spec helper.

How do you write a main spec helper to make your life easier as testing the app grows more complex?

Most test suites will eventually need setup steps as your project starts to do more complex things.  You can also use this file to add global methods for mocha, or chai, or other custom global methods.  It’s also useful to do any plugin setup in the helper file.

Things we sometimes include are a couple of global method definitions.  (This example requires lodash)

function def(name, func) {
  beforeEach(name, function() {
    _.set(this, name, func.call(this, this));
  });
}

def allows you to define values in the context of a test.

Example:

def(‘done’, testInstance => function() {});
def(‘wrapper’, ({ done }) => mount(<Component done={done}/>));

We export this for use by using global.def = defin the helper file to export the utility.

If you carefully follow these instructions, you should be up and running and be ready to start making assertions about your app. Check out our getting started with mocha and chai posts for more on that. Let us know on Twitter if there’s anything we should add or cover!

*Natasha Osborne contributed to this post.

Leave a Reply