Testing with Webpack 2, inject-loader, karma, mocha, chai and sinon

Perry Mitchell / 2016-12-06 16:37:19
Testing with Webpack 2, inject-loader, karma, mocha, chai and sinon

Webpack is a pow­er­ful build tool per­fectly suited to build­ing lay­ered, com­plex JavaScript ap­pli­ca­tions that make use of lots of dif­fer­ent tools, styles and me­dia. It uses a webpack.config.js con­fig­u­ra­tion file to find the files and con­tent it needs for com­pil­ing the fi­nal build out­put(s). Because the con­fig­u­ra­tion is a reg­u­lar JavaScript file, it al­lows de­vel­op­ers to be very ex­pres­sive and cre­ative with how they spec­ify their ap­pli­ca­tion’s in­puts and out­puts.

There’s a num­ber of ways to do it, but web­pack can be con­fig­ured to be­have dif­fer­ently for each en­vi­ron­ment you have - for in­stance, your test­ing con­fig­u­ra­tion will ob­vi­ously dif­fer in some ways to your pro­duc­tion build. You may have use for a build that is very close to pro­duc­tion’s that you use for test­ing, but chances are your pro­duc­tion build will be mini­fied (and mini­fi­ca­tion can take some time). Already you have sev­eral dif­fer­ent con­fig­u­ra­tions you want to han­dle, and be­cause web­pack reads the con­fig­u­ra­tions from the ex­ports of the con­fig­u­ra­tion file, you can spec­ify dif­fer­ent pa­ra­me­ters based upon the en­vi­ron­ment.

So web­pack sounds good for test­ing, but what about the rest of our har­ness? Choosing from some of the most pop­u­lar tools, I’ve gone with mocha, chai and sinon (there are tonnes of test­ing tools, but I know that these play nicely to­gether). Mocha is a test­ing frame­work that pro­vides the struc­ture for spec­i­fy­ing and set­ting-up your tests. Chai is an as­ser­tion li­brary that gives you a swiss army knife of ex­pec­ta­tions you can use to con­strain the be­hav­iour of your ap­pli­ca­tion. Sinon is a mock­ing li­brary that helps wrap your tests up into a pur­pose­ful and con­trol­lable unit of func­tion­al­ity - it pro­vides many meth­ods of stub­bing de­pen­den­cies for the pur­pose of test­ing in­ter­con­nected com­po­nents.

The amaz­ing team be­hind web­pack have re­leased ver­sion 2 in beta, and it’s def­i­nitely the way to go in terms of speed and con­fig­u­ra­tion ex­pres­sion. Of course it’s beta and should be used care­fully, but if you know how to lock down your de­pen­den­cies prop­erly with re­stric­tive semver ranges or shrinkwrap the risk is min­i­mal. This post will be re­fer­ring to ver­sion 2 (2.1.0-beta.27) and how best to get it work­ing with the other tools I’ve al­ready men­tioned. Let’s jump straight into in­stalling our re­quired pack­ages…

Firstly, let’s in­stall web­pack by run­ning npm install webpack@2.1.0-beta.27 --save. We’re go­ing to be work­ing with ES6 in this pro­ject, so let’s in­clude ba­bel as well: npm install babel-core babel-loader babel-preset-2015 --save.

We can also in­stall our test­ing tools now as well by run­ning the fol­low­ing:

npm install mocha chai sinon karma karma-chai karma-mocha karma-sinon karma-webpack karma-chrome-launcher --save-dev

I’ll even give you a sneak-peak at our package.json so you can see how I’ve setup the pro­ject:

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "entry.js",
  "scripts": {
    "build": "webpack --progress",
    "test": "NODE_ENV=testing karma start --single-run",
    "test:watch": "NODE_ENV=testing karma start"
  },
  "dependencies": {
    "babel-core": "^6.18.2",
    "babel-loader": "^6.2.8",
    "babel-preset-es2015": "^6.18.0",
    "webpack": "2.1.0-beta.27"
  },
  "devDependencies": {
    "chai": "^3.5.0",
    "inject-loader": "2.0.1",
    "karma": "^1.3.0",
    "karma-chai": "^0.1.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-mocha": "^1.3.0",
    "karma-sinon": "^1.0.5",
    "karma-webpack": "^1.8.0",
    "mocha": "^3.2.0",
    "sinon": "^1.17.6"
  }
}

Doesn’t look so crazy, right? The bare setup that we’ve just cre­ated is very ba­sic, and there’s a large num­ber of other load­ers and tools that will most likely be added as your pro­ject comes to­gether.

A loader is a con­tent han­dler for web­pack. It takes con­tent (JavaScript, text, HTML, SASS, CSS, im­ages etc.) as in­put and out­puts a trans­formed copy that web­pack wraps into the fi­nal bun­dles. It al­lows us to trans­form con­tent into dif­fer­ent for­mats for bet­ter con­sump­tion by clients, like how ba­bel trans­forms ES6 code into ES5 (babel-loader).

The ap­pli­ca­tion

Let’s in­tro­duce our ap­pli­ca­tion - it’s a ba­sic one, but it’s enough to rep­re­sent a ba­sic use-case of web­pack and load­ers:

First up is a lit­tle helper file called filea.js - imag­i­na­tive right? Hardly:

export function getSpecialValue() {
    return 10;
}

We can see that getSpecialValue() sim­ply re­turns 10, and that the func­tion is ex­ported for use else­where.

Our pri­mary file entry.js is re­spon­si­ble for all the setup in the li­brary:

import { getSpecialValue } from "filea";

export function getValue() {
    return getSpecialValue() * 2;
}

Our main file im­ports only getSpecialValue from filea.js and uses it in its own func­tion getValue().

The di­rec­tives import and export are what ES6 uses to han­dle de­pen­dency load­ing. They’re not avail­able in browsers yet, and even if they were, it’d be a very long time un­til a ma­jor por­tion of users had browsers that sup­ported them. Because if this, we need to tran­spile them to a pat­tern like com­monjs that will work in the browsers of to­day. Webpack 2.x has na­tive sup­port for this process, whereas ver­sion 1.x did not.

Looking at our ap­pli­ca­tion we can see that we have a tight de­pen­dency on filea.js from entry.js. We can very eas­ily test filea.js as a unit, but be­cause entry.js de­pends upon an­other file for its func­tion­al­ity, we need to get a bit cre­ative when test­ing it. Before that, though, we need to build them prop­erly into their fi­nal form.

Building

Below is a ba­sic web­pack 2 con­fig­u­ra­tion file (webpack.config.js):

const path = require("path");
const webpack = require("webpack");

module.exports = {

    entry: {
        script: path.resolve(__dirname, "./entry.js")
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                use: "babel-loader",
                exclude: /(\/node_modules\/|test\.js|\.spec\.js$)/
            }
        ]
    },

    output: {
        path: "./dist",
        filename: "script.js",
        pathinfo: true
    },

    resolve: {
        extensions: [".js"],
        modules: [
            __dirname,
            path.resolve(__dirname, "./node_modules")
        ]
    }

};

There’s re­ally not much go­ing on here, and web­pack does all of the hard work for us by:

  • Finding our en­try file(s)
  • Finding all files ref­er­enced by it (using the resolve sec­tion to help it)
  • Piping all found con­tent and source files through the module.rules sec­tion to find which loader(s) should be used to con­vert it
  • Rendering all processed con­tent to the output file(s)

Before ba­bel will work prop­erly, we need to also tell it what pre­sets to use when com­pil­ing. We can do this by giv­ing it an en­vi­ron­ment-aware .babelrc file:

{
    "env": {
        "testing": {
            "presets": [
                "es2015"
            ]
        }
    },
    "presets": [
        ["es2015", { "modules": false }]
    ]
}

In our con­fig­u­ra­tion we want ba­bel to han­dle mod­ules dif­fer­ently in pro­duc­tion com­pared to test­ing. Because web­pack 2 sup­ports import and export, we don’t need ba­bel to han­dle these in pro­duc­tion, and we can set the mod­ules pa­ra­me­ter to false. The rea­son we do want ba­bel to han­dle these di­rec­tives in test­ing will be cov­ered later, and has to do with inject-loader.

Once this is con­fig­ured we can use npm run build to build our pro­duc­tion-ready (not quite 😬) script.

entry

This has­n’t changed so much since 1.x, and it al­lows us to spec­ify the en­try points into our ap­pli­ca­tion. Webpack works by scan­ning source files for ref­er­ences to other files, and then con­tin­ues by scan­ning them also. Weback will in­clude all ref­er­enced files in the build out­put pro­vid­ing that the en­tries con­nect to them. You can have mul­ti­ple en­tries which will build mul­ti­ple out­puts.

module.rules

This was orig­i­nally module.loaders, but module.rules now al­lows us to write a more in­tu­itive rule­set for our con­tent load­ers. This is where we spec­ify what file types are processed by what load­ers. We’re only us­ing JavaScript for this pro­ject, so we can pipe all *.js files into babel-loader.

output

This also has­n’t changed much - we can sim­ply spec­ify an out­put di­rec­tory and file­name. pathinfo sim­ply in­cludes com­ments about which file a import->require con­verted state­ment refers to.

resolve

The re­solve sec­tion al­lows us to spec­ify some rules for web­pack to fol­low to help it find source files. Ours is pretty generic, and it also has­n’t changed much from 1.x.

Testing setup

It may look like we have a lot to con­fig­ure for the test­ing phase, with sinon, mocha, chai and karma to deal with, but hon­estly there’s not much go­ing on here ei­ther. All of these tools work so well to­gether, most of the setup time is spent spec­i­fy­ing glob pat­terns for find­ing source and spec files.

We need to start by cre­at­ing a karma con­fig­u­ra­tion by run­ning karma init (karma prob­a­bly is­n’t in­stalled glob­ally, as we have it in our lo­cal mod­ules, so this com­mand would ac­tu­ally be ./node_modules/.bin/karma init). This will cre­ate a karma.config.js file in the cur­rent di­rec­tory. Karma will prompt you for some en­vi­ron­ment-spe­cific in­for­ma­tion, such as which browser(s) you want to test in, and what frame­works you want to test with. If you miss some­thing it’s ok, you can just add it to the con­fig­u­ra­tion file.

After some tweak­ing, it might look some­thing like this:

module.exports = function (config) {
    config.set({
        basePath: '',
        frameworks: ['mocha', 'chai', 'sinon'],
        files: [
            './*.spec.js'
        ],
        exclude: [],
        preprocessors: {
            "./*.spec.js": ["webpack"]
        },
        // webpack configuration
        webpack: require("./webpack.config.js"),
        webpackMiddleware: {
            stats: "errors-only"
        },
        reporters: ['progress'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['ChromeWithoutSecurity'],
        customLaunchers: {
            ChromeWithoutSecurity: {
                base: 'Chrome',
                flags: ['--disable-web-security']
            }
        },
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: false,
        concurrency: Infinity
    });
};

The im­por­tant ar­eas to check are files, preprocessors, webpack and webpackMiddleware. We need to spec­ify our test files in the files sec­tion (these are au­to­mat­i­cally used as en­try points for web­pack), as well as in the preprocessors sec­tion (for web­pack han­dling). We can con­fig­ure web­pack in karma (thanks to the karma-web­pack mod­ule) by pass­ing the we­back con­fig­u­ra­tion in the webpack prop­erty. webpackMiddleware al­lows us to con­fig­ure some web­pack-spe­cific op­tions, like keep­ing the con­sole out­put clean.

We’ll leave singleRun as false to al­low for file-watch­ing - we can over­ride this in our npm scripts in package.json.

Let’s look at our first test - filea.spec.js:

import { getSpecialValue } from "filea";

describe("filea", function() {

    describe("getSpecialValue", function() {

        it("returns a special value", function() {
            expect(getSpecialValue()).to.equal(10);
        });

    });

});

It’s su­per ba­sic, and runs only a sin­gle test against the getSpecialValue func­tion. We can use import here be­cause web­pack knows how to use it to find source ref­er­ences.

The BDD struc­ture is pro­vided to us by mocha (describe/it) and al­lows us to write mean­ing­ful struc­tures around our test­ing code. I per­son­ally like re­fer­ring to the tested mod­ule in the top­most describe block, and then nest­ing the func­tion-spe­cific blocks un­der that.

Mocha is ex­tremely pow­er­ful and has a wide range of helpers such as before, after, beforeEach and afterEach that are ter­ri­bly use­ful when it comes to com­mon setup code for a num­ber of specs (it).

Mocking by in­jec­tion

We’ve got one last task to ac­com­plish, and that’s the en­try file’s de­pen­dency on filea.js. Because the only ref­er­ence to getSpecialValue is from within the getValue func­tion, there’s no way to mock it us­ing our cur­rent toolkit, and that’s where in­ject-loader comes in.

inject-loader al­lows us to mock de­pen­dency re­quire­ments within a file. entry.js is a mod­ule that re­quires an­other, and we can use inject-loader to wrap its re­quest to getSpecialValue so that we can mock its re­turn value and de­tect that it’s ac­tu­ally called.

inject-loader does­n’t need to be setup within web­pack, but just sim­ply needs to be in­stalled.

I’ve had some prob­lems get­ting inject-loader to work with web­pack 2, but thanks to the owner (and sev­eral other peo­ple - thanks!), we got it work­ing with ver­sion 2.0.1.

Take a look at our fi­nal test file - entry.spec.js:

describe("entry", function() {

    beforeEach(function() {
        let fileInjector = require("inject-loader!entry");
        // the mock
        this.filea = {
            getSpecialValue: function() {
                return 1;
            }
        };
        // create mocked module
        this.entry = fileInjector({
            "filea": this.filea
        });
        // attach spy
        sinon.spy(this.filea, "getSpecialValue");
    });

    it("works without override", function() {
        expect(require("entry.js").getValue()).to.equal(20);
    });

    it("overrides", function() {
        expect(this.entry.getValue()).to.equal(2);
    });

    it("gets a special value from filea", function() {
        this.entry.getValue();
        expect(this.filea.getSpecialValue.calledOnce).to.be.true;
    });

});

I’m us­ing a beforeEach block to do sev­eral things: firstly I cre­ate the in­jec­tor method:

let fileInjector = require("inject-loader!entry");

This func­tion, when called, will re­turn a new ver­sion of filea’s ex­ports with mocks in place where I spec­ify. It takes an ob­ject pa­ra­me­ter which is used to pro­vide the mocked mod­ule calls. If I want to mock the filea mod­ule, then I pass a prop­erty to the in­jec­tor method called filea which con­tains all of the func­tion mocks I wish to ap­ply - in this case, just getSpecialValue. I re­turn a dif­fer­ent value than what it would reg­u­larly re­turn to de­ter­mine that I can mock the in­ter­face (this is­n’t re­ally nec­es­sary in a real-world sce­nario, as it’s just for demon­stra­tion).

After ap­ply­ing the mock and re­ceiv­ing a mocked copy of filea (this.entry), I can at­tach a spy to the getSpecialValue method us­ing sinon. In the last spec I can sim­ply check that the calledOnce prop­erty of the spy is true to en­sure that getValue did in­deed call getSpecialValue.

Going for­ward

Hopefully this walk­through shows web­pack 2 for what it re­ally is - a pow­er­ful, pro­fes­sional, and down­right easy-to-use build tool that, when ac­com­pa­nied by the right test­ing util­i­ties, can make for a highly ro­bust and ex­pand­able ap­pli­ca­tion base. I love us­ing it for all of my pro­jects (even at work - yes we build our pro­duc­tion code with this!) and I’ve barely scratched the sur­face of its po­ten­tial.

All of the code I’ve used in this ar­ti­cle is avail­able in this gist.

Bonus

If you’ve used inject-loader be­fore (or any in-line loader), than you’re prob­a­bly fa­mil­iar with the old in-line syn­tax of web­pack 1.x:

let fileInjector = require("inject!entry");

You can get this work­ing again with­out edit­ing all of your specs by adding the fol­low­ing sec­tion to your web­pack 2.x con­fig­u­ra­tion file:

{
    resolveLoader: {
        moduleExtensions: ['-loader']
    }
}