How to set up a test runner for modern JavaScript using Webpack, Mocha, and Chai
We've all been there: You're about to build another front-end feature. You
know you want to start unit testing your JavaScript. You know that because
React employs one-way data binding, it means writing tests is made easier than
the Backbone MVC days of yore. But the setup... oh my, the setup. It's
painful. There are so many tools, so much boilerplate. So you say to yourself,
we'll do it next sprint.
But then the regressions start mounting. Your team is frustrated when QA sends
back your work and tells you the new thing works, but that you broke 2 old
things. And so now you're back to the grind, trying to ship a working build
before the end of the week.
We've all been there, but let's put our procrastination to rest once and for all. The truth
is, JavaScript testing is more awesome than ever. It might not be as distilled
as say, Rails testing. But after reading this guide, you'll be able to go back
to your team and proudly say this is the week you start testing your
JavaScript.
If you've already read the guide, or just want to play around with some
real, working code, I've prepared an example app here:
Webpack+Mocha+Chai Example
Tools
Right now, the landscape of tools for testing JavaScript is large. In this
guide, we're going to focus on what I've found to be the most productive
combination:
-
Mocha to run our tests.
-
Chai to make assertions.
-
Webpack to glue everything together.
Install Packages
I'll assume you're already familiar with npm, have created a package.json
file, and are using it in your project. If not, here's a tutorial to get you
started.
The npm command installs packages you want to use in your application and
provides an interface for working with them. We're going to install the
packages that will support our tests. Because these packages are for our
development use only, we use the --save-dev
option when running npm:
npm install --save-dev webpack mocha chai mocha-webpack
Create a Webpack Configuration
Webpack is a module bundler for the web. You
might have used Browserify or CommonJS in the past to modularize your
JavaScript. Webpack takes this paradigm a step further and lets you produce a
dependency for just about any type of file. A full explanation of the tool is
outside the scope of this tutorial, but Ryan Christiani has a great
Introduction to Webpack
tutorial to get you started.
For now, create a file webpack.config.js
and fill it with the following:
var webpack = require('webpack');
module.exports = {
module: {
loaders: [
{
test: /.*\.js$/,
exclude: /node_modules/,
loaders: ['babel']
}
]
},
entry: 'index.js',
resolve: {
root: [ __dirname, __dirname + '/lib' ],
extensions: [ '', '.js' ]
},
output: {
path: __dirname + '/output',
filename: 'app.bundle.js'
}
};
Configure Babel
Babel is a JavaScript compiler that allows us to use
next generation JavaScript (ES6, ES7, etc) in browsers that only support
ES5. As you'll see when we begin writing tests, having ES6 import
statements and fat arrow function notation (() => { }
) will make our tests
more readable and require less typing.
You'll notice, in the loaders
section above, we're using the babel
loader
to process our JavaScript. This will allow us to write our application and
test code in ES6. However, Babel requires that we configure it with presets,
which will tell Babel how it should process our input code.
For our example, we need just one preset: es2015
. This tells Babel we want to
use the ECMAScript 2015 standard so we can
use things like the import
and export
statements, class
declarations, and
fat arrow (() => {}
) function syntax.
To use the preset, we'll first install its package using npm
:
npm install --save-dev babel-preset-es2015
Then, we'll tell Babel to use it by creating a .babelrc
file:
{
"presets": [
"es2015"
]
}
Create the entry file and test Webpack configuration
Our Webpack configuration states that our entry file, the JavaScript module
Webpack will run when our bundle is included in the page, is index.js
. So
let's create that file now. For now, let's just alert "Hello, World!". We're
not going to run this code anyway, since we're really just using this entry
file to be sure Webpack is configured properly.
// index.js
alert("Hello, World!");
Then we'll create an output
directory. This is where we've configured
Webpack to write our bundle file:
If we've configured everything properly, running Webpack should spit out our
bundle file:
If the file output/app.bundle.js
is present and you can locate our
alert("Hello, World!")
code in its contents, then you've configured Webpack
successfully!
Set up the Mocha runner command
NPM has a scripts
configuration option that allows creating macros for
running common commands. We'll use this to create a command that will run our
test suite on the command line.
In your package.json
file, add the following key to the JSON hash:
{
"scripts": {
"test": "mocha-webpack --webpack-config webpack.config.test.js \"spec/**/*.spec.js\" || true"
}
}
For an actual example of this command in a real package.json
file, see the package.json file in the example
code.
Dang though, that is one hefty command. Let's go through this piece by piece.
First, we're assigning this to the test
command. That means that when we run
npm run test
, NPM will execute the mocha-webpack --webpack-config ...
command for us.
The mocha-webpack
executable is a module that precompiles your Webpack
bundles before running Mocha, which actually runs your tests. Now,
mocha-webpack is designed for server-side code, but so far I haven't had any
problems using it for client-side JavaScript. Your mileage may vary.
When we call the mocha-webpack
command, we pass it the --webpack-config
option with the argument webpack.config.test.js
. This tells mocha-webpack
where to find the Webpack configuration file to use when precompiling our
bundle. Notice that the file has a .test
suffix and that we haven't created
it yet. We'll do that in the next step.
After that, we pass mocha-webpack
a glob of our test files. In this case,
we're passing it spec/**/*.spec.js
, which means we'll run all the test files
contained within the spec
folder and all folders within it.
And finally, we append || true
to the end of the command. This tells NPM
that in the event of an error (non-zero) exit code from the mocha-webpack
command, we shouldn't assume something horrific went wrong and print a
lengthy error message explaining that something probably did. Most of the time
we run tests, a test or few will fail, resulting in a non-zero exit status.
This addition cleans up our output a bit so we don't have to read a nagging
error message each time. I'm sure the NPM team meant well when they added this
message, but I think it's a bit silly we have to resort to this to remove it.
If you know a better way, leave a comment!
Create our test Webpack configuration
Because we're running our tests on the command line and not in the browser, we
need to be sure to tell Webpack that our target environment is Node and not
browser JavaScript. To do this, we'll create a specialized test Webpack
configuration which targets Node in webpack.config.test.js
:
var config = require('./webpack.config');
config.target = 'node';
module.exports = config;
I also want to point out how nice it is that Webpack configurations are just
plain JavaScript objects. We're able to require our base configuration, set
the target
property, and then export the modified configuration. This
pattern is especially useful when producing production configuration files,
but that's a topic for another guide.
Write a basic test
It's the moment we've been waiting for! We've laid the foundation for testing
in our project. Now let's write a basic (failing) test to see Mocha in action!
Create the spec
directory in your project if you haven't already. Before we
get testing React components, let's just try our hand at testing a plain old
function. Let's call that function sum
, and test that it does indeed sum two
numbers. I know, it's real exciting. But it'll give us confidence our
test setup is working.
Create a file spec/sum.spec.js
with the following:
import sum from 'sum';
import { expect } from 'chai';
describe("sum", () => {
context("when both arguments are valid numbers", () => {
it("adds the numbers together", () => {
expect(sum(1,2)).to.equal(3);
});
});
});
Let's go over that one line at a time.
First, we import a function called sum
from a module called 'sum'
.
You probably guessed we're going to need to create that file. You guessed
right.
Create the file lib/sum.js
:
export default function() { }
Note that we're creating the file inside the lib
folder. Way back in step 2,
we told Webpack that we should resolve modules in both the root folder as well
as the /lib
folder. We use lib
because it indicates to other developers
that this file is part of our application library code, as opposed to a
test, or configuration, or our build system, etc.
Assertion Styles
The second line in our test file imports a function expect
from the Chai
module. Chai has a couple different assertion
styles which dictate how tests will be written.
Without going too far into the details, it means your tests could either read
like this:
Or like this:
Or like this:
This is largely a matter of developer preference. In my time as a developer,
I've seen the Ruby community shift its consensus from assert
, toward should
,
and now toward expect
. So let's settle on expect
for now.
Run our test suite
Now that we've created our spec/sum.spec.js
file, let's go ahead and run our
npm run test
command:
npm run test
> react-webpack-testing-example@1.0.0 test /Users/teejayvanslyke/src/react-webpack-testing-example
> mocha-webpack --webpack-config webpack.config.test.js "spec/**/*.spec.js" || true
sum
when both arguments are valid numbers
1) adds the numbers together
0 passing (7ms)
1 failing
1) sum when both arguments are valid numbers adds the numbers together:
AssertionError: expected undefined to equal 3
at Context.<anonymous> (.tmp/mocha-webpack/01b73f0d4e3c95d9c729f459c86e1fc4/01b73f0d4e3c95d9c729f459c86e1fc4-output.js:93:61)
Success! Well, sort of. Our test runs, but it looks like it's failing because we
never implemented the sum
function. Let's do that now.
Make the test pass
Let's make our sum function take two arguments, a
and b
. We'll return the
result of adding both of them together, like so:
export default function(a, b) { return a + b; }
Now run our test again. It passes!
npm run test
> react-webpack-testing-example@1.0.0 test /Users/teejayvanslyke/src/react-webpack-testing-example
> mocha-webpack --webpack-config webpack.config.test.js "spec/**/*.spec.js" || true
sum
when both arguments are valid numbers
✓ adds the numbers together
1 passing (6ms)
Watch for changes to streamline your workflow
Now that we've written a passing test, we'll want to iterate on our math.js
library. But rather than running npm run test
every time we want to check the
pass/fail status of our tests, wouldn't it be nice if it ran automatically
whenever we modified our code?
Mocha includes a --watch
option which does exactly this. When we pass
mocha-webpack
the --watch
option, Mocha will re-run our test suite whenever
we modify a file inside our working directory.
To enable file watching, let's add another NPM script to our package.json
:
{
"scripts": {
"test": "mocha-webpack --webpack-config webpack.config.test.js \"spec/**/*.spec.js\" || true",
"watch": "mocha-webpack --webpack-config webpack.config.test.js --watch \"spec/**/*.spec.js\" || true"
}
}
Notice how the watch
script just runs the same command as the test
script,
but adds the --watch
option. Now run the watch
script:
Your test suite will run, but you'll notice the script doesn't exit. With the
npm run watch
command still running, add another test to spec/sum.spec.js
:
import sum from 'sum';
import { expect } from 'chai';
describe("sum", () => {
context("when both arguments are valid numbers", () => {
it("adds the numbers together", () => {
expect(sum(1,2)).to.equal(3);
});
});
context("when one argument is undefined", () => {
it("throws an error", () => {
expect(sum(1,2)).to.throw("undefined is not a number");
});
});
});
Save the file. Mocha will have re-run your suite, and it should now report that
your new test fails.
Reduce duplication in package.json
In the previous step, we copied and pasted the test
script into the watch
script. While this works fine, copy and paste should bother every developer just
a little bit.
Luckily, mocha-webpack
provides a way to specify the default options to the
command so we needn't include them in each line of our package.json
's
scripts
section.
Create a new file called mocha-webpack.opts
in your project's root directory:
--webpack-config webpack.config.test.js
"spec/**/*.spec.js"
And now, your package.json
file can be shortened like this:
{
"scripts": {
"test": "mocha-webpack || true",
"watch": "mocha-webpack --watch || true"
}
}
Helpful links