Unit Testing and Writing Testable Code

What do we want?!
Front-end unit tests!
When do we want it?!
For several years, now!

So why is it that for all the talk of the importance of unit testing, we rarely encounter it in the workplace? Well, for one thing, fortunately, that broad generalization is on a downward slope of relevance. For the companies where this still holds true, I have a couple of theories.

One of them involves the sheer volume of legacy code still out there in production at companies that have been established on the internet for many years now. When looking at an existing code base of thousands or even tens of thousands of lines of javascript, the thought of writing unit tests to cover all that is, to say the least, a daunting task. Where do you start when there’s so much existing code that to write test coverage for it all, you’d have to shelve any hopes of feature development for the next 6 months? How do you justify that to management, PM’s, or your customers?

The other one involves a combination of code just not being written well for testing purposes, and engineers not having a good idea of what should actually be tested. Both of these things can be solved with experience, but I’ll attempt to outline some good practices here to get the ball rolling.

The obvious benefit of unit testing is that we get an instantaneous overview of whether some basic, expected functionality of our web applications has been broken by recent changes. Depending on your project, and overhead involved, you may want to run unit tests on each save, each commit, each hour, day, or as part of your continuous integration process via Jenkins or TravisCI. You can use your unit tests to verify functional outputs, DOM manipulations, event-driven behavior, or even basic markup structure.

In the case of a monstrous amount of preexisting code, there’s not much that can be done as far as unit test coverage is concerned, except to say that this is the line in the sand. From here on out, all new feature work will require unit test coverage, and any new defects that are reported will require a unit test to verify their solution. In this way, you can let your test library organically grow, while still being able to handle feature work at the same time. Nobody needs to slam on the brakes to be able to write coverage for everything, especially when a lot of it may be dead execution paths, or code that’s slated for refactoring work in the future.

In the other case, you need to be aware of what exposing for testability means. If you’re passing anonymous functions around everywhere, there’s no way to verify the results of those functions, unless they’re being passed as callbacks that you can explicitly trigger.

Let’s first start by looking at an example of some typical code you might find in any random web page on the internet that makes ajax calls through jQuery’s $.ajax function:

$('#myButton').on('click', function () {
    $.ajax({
        type: 'POST',
        data: {
            foo: 'foo',
            bar: 'bar'
        },
        success: function (json) {
            if (json.error) {
                $('#error')
                    .html(json.error)
                    .show();
                return;
            }

            $('#foo').html(json.foo);
            $('#bar').html(json.bar);
        },
        dataType: 'json'
    });
});

So we have this button, #myButton, which has a click event listener that fires off our ajax callback and populates some random elements with some random data. In addition, there’s an error property that’s checked for on the json response object, and displayed if present. Fairly straightforward stuff. But how could we test any of this?

The problem with the code, as it’s written, is that it’s using anonymous functions passed inline as callbacks. As soon as they’re passed on, they’re forgotten about, never to be seen again by the javascript compiler. We could restructure this to use named functions, and start paving the way for testability. Here’s an example to demonstrate how we might start doing that:

var onSuccess = function (json) {
    if (json.error) {
        $('#error')
            .html(json.error)
            .show();
        return;
    }

    $('#foo').html(json.foo);
    $('#bar').html(json.bar);
};

$('#myButton').on('click', function () {
    $.ajax({
        type: 'POST',
        data: {
            foo: 'foo',
            bar: 'bar'
        },
        success: onSuccess,
        dataType: 'json'
    });
});

Now that we’ve broken our success handler out into its own function expression, we can pass it by name in the success option of our ajax request, and it will behave as before. However, we now have the added bonus of being able to test this handler externally. We can feed it mock responses and verify that it behaves as expected, and if anybody goes in to modify this function in the future, we can have instant visibility into whether they broke some expected behavior of its intended functionality. Here’s an example (using QUnit):

test('Throws error', 2, function () {
    var testJson = {
        error: 'Some error occurred'
    };

    $('#foo').empty();
    $('#bar').empty();

    onSuccess(testJson);

    ok($('#error').is(':visible'), 'Error message container displayed');
    equal($('#error').text(), testJson.error, 'Error message text set propertly');
});

test('Sets content', 3, function () {
    var testJson = {
        foo: 'Foo string',
        bar: 'Bar string'
    };

    $('#foo').empty();
    $('#bar').empty();

    onSuccess(testJson);

    ok($('#error').is(':hidden'), 'Error message container not displayed');
    equal($('#foo').text(), testJson.foo, 'Foo message text set propertly');
    equal($('#bar').text(), testJson.bar, 'Bar message text set propertly');
});

Now, at any given time, we can execute our success callback with a test data object, and verify that it will display the proper #error message when that condition is right, and otherwise, display the proper #foo and #bar content. This isn’t an ideal situation for unit testing, though. It’s fairly basic, and not necessarily the kind of thing you absolutely need to be testing the fidelity of. So what makes something worth testing? Where do you draw the line between ease of development, ease of maintenance, and exposure for testability?

When it comes to the determination of what makes a good candidate for unit testing, the decision is entirely subjective. Some guidelines for functions that should be tested are any kind of public API functions, in the case of a library, framework, or reusable component of any kind that you’re publishing. You want to make sure that your interface is staying the same from release to release, unless you’re giving your consumers advanced notice that something’s going to be changing.

Other good candidates for unit tests are going to be functions that perform logical evaluations or transformations. If you have a function that counts the number of instances of something in your page, or which takes a data set as an input and returns some kind of markup for its output, these are things that you can easily test. Construct a test object or some kind of setup that can be examined by your functions, invoke the functions in question, and check their output against the expected output.

Event listeners and emitters are a couple of other good ideas for testing. If you’re working with reusable components, whether they’re visual components or add-on modules, you’re going to want to verify that they continue to function as expected from release to release, or else small bugs can spiderweb their way through more complex systems and cause major issues for you.

Let’s take a look at a simple widget built on the jQuery UI widget factory:

$.widget('ui.messenger', {
    options: {
        sticky: false, // No timeout, requires user to close
        timeout: 2000, // If not stick, how long to display
        show: $.noop, // Callback to be fired on 'show'
        hide: $.noop // Callback to be fired on 'hide',
        close: $.noop // Callback to be fired when closeButton is clicked
    },

    _create: function () {
        this._html = {
            wrapper: null,
            message: null,
            closeButton: null
        };

        this._vars = {
            timeoutId: null
        };

        this._buildMessenger();
        this._setOptions(this.options);
    },

    _setOptions: function (options) {
        var functionCheck = function (value, key) {
                if (typeof value !== 'function') {
                    delete options[key];
                }
            },
            optionMap = {
                sticky: function (value) {
                    this._html.closeButton.toggleClass('hidden', value);

                    if (this._html.wrapper.is(':visible')) {
                        if (this._vars.timeoutId && value && !this.options.sticky) {
                            clearTimeout(this._vars.timeoutId);
                        } else if (!value && this.options.sticky) {
                            setTimeout($.proxy(function () {
                                this.hide();
                            }, this), this.options.timeout);
                        }
                    }
                },
                show: functionCheck,
                hide: functionCheck,
                close: functionCheck
            };

        $.each(options, function (key, value) {
            if (typeof optionMap[key] === 'function') {
                optionMap[key].call(this, value, key);
            }
        });

        this._superApply(options);
    },

    _buildMessenger: function () {
        this._html.wrapper = $('<div></div>');

        this._html.message = $('<div></div>', {
            'class': 'content'
        }).appendTo(this._html.wrapper);

        this._html.closeButton = $('<div></div>', {
            'class': 'close-button'
        }).appendTo(this._html.wrapper);

        this._html.wrapper.insertAfter(this.element);
    },

    _attachEvents: function () {
        this._on(this._html.closeButton, {
            click: function (e) {
                if (typeof this.options.click === 'function') {
                    this.options.click.call(this.element, e);
                }

                if (!e.isDefaultPrevented()) {
                    this.hide();
                }
            }
        });
    },

    show: function (type, message) {
        this._html.wrapper
            .addClass('message ' + type)
            .html(message)
            .show();

        this._trigger('show');

        if (!this.options.sticky) {
            this._vars.timeoutId = setTimeout($.proxy(function () {
                this.hide();
            }, this), this.options.timeout);
        }
    },

    hide: function () {
        this._html.wrapper
            .hide()
            .removeClass();

        this._trigger('hide');
    },

    widget: function () {
        return this._html.wrapper;
    },

    destroy: function () {
        $.each(this._html, function (key, element) {
            element.remove();
        });
    }
});

This is a fairly generic implementation of a messenger class, which can be used to display success/info/error messages. Sample usage might be as follows:

$(':input')
    .messenger({
        sticky: true
    })
    .on('blur', function () {
        var $this = $(this);

        if (!$this.validate()) {
            $this.messenger('show', 'error', $this.data('validationError'));
        }
    });

In this example, we have all of our form elements wired up as sticky messenger widgets, with each element having previously be wired up with some form of validation widget. Each element also has a data attribute to specify a validation error message, and if it should fail validation on blur, it uses its messenger component to display that error message. We could further improve this setup by having our validation widget use our messenger widget internally, so that this sort of behavior is encapsulated from the developer.

So now that we have our widget set up, and our use case defined, how can we test this? For one thing, once our object literal has been run through the $.widget function to create our widget prototype, it’s discarded from memory. It’s still available through the prototype┬áproperty on its public namespace of $.ui.messenger, or you could even go so far as assigning it to a variable in some shared namespace (e.g. window.Widgets.Messenger), and pass that reference into the factory. So what should we actually test?

We should probably test its core options, the public API functions, and any event listeners and triggers that we have set up as part of this widget. We don’t necessarily need to test any of the private methods within the widget itself, so long as the outcome is as expected. This leaves us with the flexibility to change those internal methods as time goes on, without having to update our tests with every iteration. So long as the end result of those methods being executed is the same, that’s all we need to verify.

So let’s take a look at how we might write some tests for these things. One thing you’ll notice is my use of deferred objects in the following test code. I feel they lead to a much cleaner appearance in event-driven code, rather than winding up with that gigantic sideways tree of nested callbacks, affectionately known as Callback Hell. It also allows you to set timeouts on your tests, and to auto-fail them in a chain-reaction type of setup if some base test fails that should cause the other ones to fail. This helps speed up your overall test execution, and prevent hanging the test engine in the event of something failing partway through.

module('Messenger: Core');

test('Initialization', 1, function () {
    var messengercreate = $.Deferred(),
        messenger = $('<input />').appendTo('body');

    stop();

    messengercreate
        .done(function () {
            ok(messenger.is(':ui-messenger'), 'Successfully initialized');
        })
        .fail(function () {
            ok(false, 'Successfully initialized');
        })
        .always(function () {
            start();
        });

    messenger
        .on('messengercreate', function () {
            messengercreate.resolve();
        })
        .messenger();

    setTimeout(function () {
        messengercreate.reject();
    }, 100);
});

As you can see, we create a deferred object to hold our tests, and when we receive our automatic messengercreate event from the widget itself, we resolve our deferred object, and let the initialization check run its test. Otherwise, we set a 100ms timeout to fail the test if it hasn’t been resolved by that time. Since this is going to involve an asynchronous runtime, we also need to .stop() the test engine, and .start() it in our .always() callback.

module('Messenger: Methods');

test('Show/hide', 2, function () {
    var messengercreate = $.Deferred(),
        messengershow = $.Deferred(),
        messengerhide = $.Deferred(),
        messenger = $('<input />').appendTo('body');

    stop();

    messengercreate
        .done(function () {
            messenger.messenger('show', 'info', 'Test message');

            setTimeout(function () {
                messengershow.reject();
            }, messenger.messenger('option', 'timeout') + 100);
        })
        .fail(function () {
            messengershow.reject();
        });

    messengershow
        .done(function () {
            ok(messenger.next('.message').is('visible'), 'Messenger shown');

            messenger.messenger('hide');

            setTimeout(function () {
                messengerhide.reject();
            }, 100);
        })
        .fail(function () {
            ok(false, 'Messenger shown');
            messengerhide.reject();
        });

    messengerhide
        .done(function () {
            ok(messenger.next('.message').is(':hidden'), 'Messenger hidden');
        })
        .fail(function () {
            ok(false, 'Messenger hidden');
        });

    $.when(messengercreate, messengershow, messengerhide)
        .always(function () {
            start();
        });

    messenger
        .on('messengercreate', function () {
            messengercreate.resolve();
        })
        .on('messengershow', function () {
            messengershow.resolve();
        })
        .on('messengerhide', function () {
            messengerhide.resolve();
        })
        .messenger();

    setTimeout(function () {
        messengercreate.reject();
    }, 100);
});

In the above example, we’re now using 3 separate deferred objects to isolate our testing logic per step. We .stop() our test engine for the asynchronous workflow, and set up a $.when() deferred object to track when we can .start() the test engine again. Each step of the way, we set up a timeout to fail the test and prevent the test engine from hanging, and lastly, if one of those tests fails at any point, it will cascade failures to the rest of the deferred objects to fail them instantly instead of trying to run them, since each one is dependent on the previous one executing properly.

Lastly, we’ll take a look at how to test some of the event triggers themselves. For the purposes of this widget, we have (aside from the automatic events that come with the jQuery UI widget factory) events being broadcast for messengershow, messengerhide, and messengerclick. The messengerclick event itself is only triggered if the sticky option is truthy, and can be used to keep the messenger from hiding itself, if you call the .preventDefault() method on the event object. This way, you can have some kind of custom business logic to prevent the hide from occurring, like a confirm dialog that prompts “Are you sure you want to hide this message?”

module('Messenger: Events');

test('Show/click/hide', 3, function () {
    var messengercreate = $.Deferred(),
        messengershow = $.Deferred(),
        messengerclick = $.Deferred(),
        messengerhide = $.Deferred(),
        messenger = $('<input />').appendTo('body');

    stop();

    messengercreate
        .done(function () {
            messenger.messenger('show', 'info', 'Test message');

            setTimeout(function () {
                messengershow.reject();
            }, 100);
        })
        .fail(function () {
            messengershow.reject();
        });

    messengershow
        .done(function (e) {
            equal(e.type, 'messengershow', 'Show event type');

            messenger
                .find('.close-button')
                    .trigger('click');

            setTimeout(function () {
                messengerclick.reject();
            }, 100);
        })
        .fail(function () {
            messengerclick.reject();
        });

    messengerclick
        .done(function (e) {
            equal(e.type, 'messengerclick', 'Click event type');

            setTimeout(function () {
                messengerhide.reject();
            }, 100);
        })
        .fail(function () {
            ok(false, 'Click event type');
            messengerhide.reject();
        });

    messengerhide
        .done(function (e) {
            equal(e.type, 'messengerhide', 'Hide event type');
        })
        .fail(function () {
            ok(false, 'Hide event type');
        });

    $.when(messengercreate, messengershow, messengerclick, messengerhide)
        .always(function () {
            start();
        });

    messenger
        .on('messengercreate', function () {
            messengercreate.resolve();
        })
        .on('messengershow', function (e) {
            messengershow.resolve(e);
        })
        .on('messengerclick', function (e) {
            messengerclick.resolve(e);
        })
        .on('messengerhide', function (e) {
            messengerhide.resolve(e);
        })
        .messenger();

    setTimeout(function () {
        messengercreate.reject();
    }, 100);
});

test('Click and allow hide', 2, function () {
    var messengercreate = $.Deferred(),
        messengershow = $.Deferred(),
        messengerclick = $.Deferred(),
        messengerhide = $.Deferred(),
        messenger = $('<input />').appendTo('body');

    stop();

    messengercreate
        .done(function () {
            messenger.messenger('show', 'info', 'Test message');

            setTimeout(function () {
                messengershow.reject();
            }, 100);
        })
        .fail(function () {
            messengershow.reject();
        });

    messengershow
        .done(function () {
            messenger
                .find('.close-button')
                    .trigger('click');

            setTimeout(function () {
                messengerclick.reject();
            }, 100);
        })
        .fail(function () {
            messengerclick.reject();
        });

    messengerclick
        .done(function () {
            ok(true, 'Click event triggered');

            setTimeout(function () {
                messengerhide.reject();
            }, 100);
        })
        .fail(function () {
            ok(false, 'Click event triggered');
            messengerhide.reject();
        });

    messengerhide
        .done(function () {
            ok(messenger.next('.message').is(':hidden'), 'Hide event allowed');
        })
        .fail(function () {
            ok(false, 'Hide event type');
        });

    $.when(messengercreate, messengershow, messengerclick, messengerhide)
        .always(function () {
            start();
        });

    messenger
        .on('messengercreate', function () {
            messengercreate.resolve();
        })
        .on('messengershow', function () {
            messengershow.resolve();
        })
        .on('messengerclick', function () {
            messengerclick.resolve();
        })
        .on('messengerhide', function () {
            messengerhide.resolve();
        })
        .messenger();

    setTimeout(function () {
        messengercreate.reject();
    }, 100);
});

test('Click and cancel hide', 2, function () {
    var messengercreate = $.Deferred(),
        messengershow = $.Deferred(),
        messengerclick = $.Deferred(),
        messengerhide = $.Deferred(),
        messenger = $('<input />').appendTo('body');

    stop();

    messengercreate
        .done(function () {
            messenger.messenger('show', 'info', 'Test message');

            setTimeout(function () {
                messengershow.reject();
            }, 100);
        })
        .fail(function () {
            messengershow.reject();
        });

    messengershow
        .done(function () {
            messenger
                .find('.close-button')
                    .trigger('click');

            setTimeout(function () {
                messengerclick.reject();
            }, 100);
        })
        .fail(function () {
            messengerclick.reject();
        });

    messengerclick
        .done(function (e) {
            e.preventDefault();

            equal(e.type, 'messengerclick', 'Click event type');

            setTimeout(function () {
                messengerhide.resolve();
            }, 100);
        })
        .fail(function () {
            ok(false, 'Click event type');
            messengerhide.reject();
        });

    messengerhide
        .done(function () {
            ok(messenger.next('.message').is(':visible'), 'Hide event canceled');
        })
        .fail(function () {
            ok(false, 'Hide event canceled');
        });

    $.when(messengercreate, messengershow, messengerclick, messengerhide)
        .always(function () {
            start();
        });

    messenger
        .on('messengercreate', function () {
            messengercreate.resolve();
        })
        .on('messengershow', function () {
            messengershow.resolve();
        })
        .on('messengerclick', function (e) {
            messengerclick.resolve(e);
        })
        .on('messengerhide', function () {
            messengerhide.resolve();
        })
        .messenger();

    setTimeout(function () {
        messengercreate.reject();
    }, 100);
});

In this final example, we run 1 test to verify that our event types are as expected for show/click/hide, a second one to verify that the click event, left untouched, will lead to the hide event triggering properly, and a third one to verify that the click event, when preventing the default action on the event object, will prevent the hide event from triggering properly. Notice the reverse logic in our messengerclick.done() timeout function, where we wait 100ms and then .resolve() the hide event’s deferred object, because we define success in that case as the hide event failing to trigger within the 100ms timeout window.

Hopefully this post has given you some ideas on how you can start implementing unit tests into your own front-end code if you haven’t already. Don’t let the task scare you away, not everything requires test coverage in the immediate sense. You can start small and let your test library grow over time. The benefits of automated testing far outweigh the time it takes to write those tests, as hey give you instant visibility into whether your latest code has screwed something up in the existing paradigm of your project. As always, feel free to leave any questions or general comments in the section below.

2 thoughts on “Unit Testing and Writing Testable Code

  1. Pingback: Unit Testing and Writing Testable Code | JavaSc...

  2. Pingback: Unit Testing and Writing Testable Code | JS App...

Leave a Reply

Your email address will not be published. Required fields are marked *