blog post

Sep 11, 2017

How to use Behaviour Driven Development in JavaScript

Behaviour Driven Development (or BDD) is one of the tactics we use with automated testing and it can help to create code that is almost guaranteed to work in a production environment. We normally automate more complicated behaviour such as user journeys, but a simple JavaScript example will show the principles.

To show Behaviour Driven Development in a real-world example, we're going to create a jQuery plugin that helps manage AMCSS attributes.

The test runner

There are many good libraries around that can help, but I'm most familiar with Mocha and Chai. I'm a fan of Mocha's syntax and I feel that writing "it should" as we're describing the code helps us explain the behaviour without getting bogged down in the implementation.

Setting up a test runner page isn't complicated but it still requires a couple of steps.

1. Install Mocha and Chai

Pick your favourite package manager and install them, as well as a task runner and Mocha Phantom JS so we can run the Behaviour Driven Development tests through the command line in the future.

$ yarn add mocha
$ yarn add chai
$ yarn add gulp
$ yarn add gulp-mocha-phantomjs

2. Link to the files

Create your testrunner.html file. Start by adding the Mocha and Chai styles and scripts.

You then need to add the element which will contain the results.

<div id="mocha"></div>

Add some setup functionality for the test runner.


if (window.initMochaPhantomJS) {
    window.initMochaPhantomJS();
}

window._TEST_MODE = true;
mocha.setup({
    ui: "bdd"
});

After this script tag, you'll add your Behaviour Driven Development tests. I recommend keeping each file describing only a single function or object. After your tests, you'll start the tests running.


if (window.mochaPhantomJS) {
    mochaPhantomJS.run();
} else {
    mocha.run();
}

If you got everything right, you should be able to open the test runner file and see 0 passes and 0 failures.

Empty BDD tests

Having set up the test runner, we can get to work on the Behaviour Driven Development tests.

Writing the first behaviour

AMCSS recommends that the attributes have the namespace "am-" but they accept that "data-am-" may be necessary to ensure the markup validates. Whatever the prefix, updating it shouldn't require multiple changes to the code. At times, we may have the complete attribute name and our function shouldn't prefix it again. The function we write would end up working like the example below.

$.AM_PREFIX = "am-";
$.normaliseAm("button"); // -> "am-button"
$.normaliseAm("am-button"); // -> "am-button"

We can now really start with Behaviour Driven Development by writing the behaviour tests. We describe the function and explain what it should do.

describe("$.normaliseAm", function () {

    var assert = chai.assert;

    beforeEach(function () {
        $.AM_PREFIX = "am-";
    });

    it("should prefix an attribute", function () {

        var attribute = "button";
        var expected = $.AM_PREFIX + attribute;

        assert.equal($.normaliseAm(attribute), expected);

    });

    it("should not prefix an already prefixed attribute", function () {

        var expected = $.AM_PREFIX + "button";

        assert.equal($.normaliseAm(expected), expected);

    });

    it("should reflect a change to the prefix", function () {

        $.AM_PREFIX = "data-am-";
        var attribute = "button";
        var expected = $.AM_PREFIX + attribute;

        assert.equal($.normaliseAm(attribute), expected);

        $.AM_PREFIX = "";
        assert.equal($.normaliseAm(attribute), attribute);

    });

});

Notice how in each test we derive the result rather than hard-coding it. This helps avoid typos and keeps our focus on the behaviour rather than the implementation. Redundant as it sounds, it also means that we don't have to test our tests.

If we were to run our Behaviour Driven Development tests now then everything would fail - we haven't written the code yet. It's worth doing anyway just to make sure our test runner works.

BDD tests failing

Developing towards the behaviour

With our Behaviour Driven Development tests in place, we can write the code that satisfies those conditions.

$.AM_PREFIX = "am-";

$.normaliseAm = function (attribute) {

    var prefix = $.AM_PREFIX;

    return attribute.indexOf(prefix) === 0
        ? attribute
        : prefix + attribute;

};

With code written we run our tests again, fixing any issues that we come across until all our tests pass.

BDD tests passing

Adding behaviours

The tricky part about Behaviour Driven Development is that you don't just have to plan for your own behaviour, you should think about other developers and edge cases. For example, I'm always going to pass a string to my function, but would another developer always do the same, intentionally or otherwise? I feel the function shouldn't throw an error in those cases. While we're thinking about other developers, American developers would spell "normalise" with a "Z" rather than an "S" - we should take that into account.

describe("$.normaliseAm", function () {

    var assert = chai.assert;
    var tests;

    beforeEach(function () {

        $.AM_PREFIX = "am-";

        tests = [
            "a",
            0,
            {},
            [],
            undefined,
            null,
            function () {},
            true
        ];

    });

    // ...

    it("should not throw an error if passed a non string", function () {

        assert.doesNotThrow(function () {
            tests.forEach($.normaliseAm);
        });

    });

    it("should always return a string", function () {

        tests.forEach(function (test) {
            assert.isTrue(typeof $.normaliseAm(test) === "string");
        });

    });

    it("should have the alias $.normalizeAm", function () {
        assert.equal($.normaliseAm, $.normalizeAm);
    });

});

To satisfy our new behaviours, we need to make some changes to the code. I've deferred the string interpretation to another function called interpretString so that we can test that function separately.

$.normaliseAm = function (attribute) {

    var prefix = $.AM_PREFIX;
    var interpretted = interpretString(attribute);

    return interpretted.indexOf(prefix) === 0
        ? interpretted
        : prefix + interpretted;

};

$.normalizeAm = $.normaliseAm;

We go through the same steps to write the interpretString function and we run the final tests.

All BDD tests passing

Benefits of Behaviour Driven Development

As stated at the beginning of this article, we now have a function that we can deploy to a production environment with the confidence that it won't break. By carefully setting up our Behaviour Driven Development test runner file, the tests can double as unit tests which we can run from the command line, possibly with the aid of a task runner like Gulp.

Command line BDD tests passing

Any future changes will be automatically checked against these tests (and possibly other ones as well) so we can be confident that our function won't break in the future. You can see the complete plugin (including some more tests) on GitHub.

Awwwards