Writing Testable Frontend Javascript Part 2 – Refactor away anti-patterns

This is the second of a two part introduction on how to write testable Javascript UI code.

The first article, Anti-Patterns and their fixes, uses a sample application to introduce several common, avoidable, and test-inhibiting anti-patterns. Why these common practices are anti-patterns and how to fix them are explained.

This article continues by refactoring the original application to be easier to read, easier to reuse, and easier to test. Once refactoring is complete, testing begins: a test harness is created, an XHR mock developed, and finally, a full unit test suite is added.

Use best practices to write testable UI code

The first article, Anti-patterns and their fixes, lists several best practices to make UI code testable:

Using this list as a guide, the original application is completely refactored with the goal of adding tests.

Start with the HTML – externalize all scripts

The original inline Javascript has been externalized and placed into two files: authentication-form.js and start.js. The bulk of the original logic is contained within the module authentication-form.js. The application is initialized in start.js.

Excerpt from index.html

Modules encapsulate logic with the option of a public interface

AuthenticationForm is a publicly accessible module that neatly encapsulates the majority of the original logic. AuthenticationForm provides a public interface through which functionality can be tested.

A public interface – excerpt from authentication-form.js

Use instantiable objects

The original form was not instantiable, its code was meant only to be run once. Effective unit testing was nearly impossible. The refactored AuthenticationForm is a prototype object; new instances are created using Object.create.

Instantiable object – excerpt from authentication-form.js

Flatten the pyramid

The refactored AuthenticationForm extracts logic from the original pyramid of doom into four publicly accessible functions. Functions are provided for object initialization and destruction, a further two are available in its testing interface.

Flattened pyramid – excerpt from authentication-form.js

Separate DOM event handlers from the action it performs

Separate DOM event handlers from the action it performs to help make action logic reusable and testable.

DOM events are separated from their actions – excerpt from authentication-form.js

Use callbacks (or some other notification mechanism) in asynchronous functions

The two functions in AuthenticationForm’s testing interface, submitForm and checkAuthentication, are asynchronous. Both accept an optional callback to invoke when all processing is complete.

Asynchronous functions with callbacks – excerpt from authentication-form.js

Clean up after your objects

Unit tests should run in isolation of each other. Any state, including attached DOM event handlers, must be reset between tests.

Attached DOM event handlers are removed – excerpt from authentication-form.js

Separate application initialization into its own module

start.js is a self-invoking function that is run after all other Javascript has loaded. Since our application is very simple, only a small amount of initialization is needed – a single AuthenticationForm instance is created and initialized.

start.js

At this point, the entire original application has been refactored and re-implemented. Users should see zero change in functionality, modifications are purely under the hood.

What about the tests?

Even though our code is now testable, an article on unit testing is incomplete without actually writing some tests! Several high quality unit test frameworks exist, QUnit is used for this example.

First, a test harness is needed. A test harness is composed of a mock DOM and Javascript code. A mock DOM consists of elements that are used during testing, usually things like form elements or elements that you check the visibility of. To avoid cross test pollution, the mock DOM is reset after every unit test. QUnit expects the mock DOM to be in the #qunit-fixture element.

Javascript code includes a unit test runner, the code being tested, mock dependencies, and the tests themselves.

Test Harness – excerpt from tests/index.html

Write the XHR mock

XHR requests introduce a dependency on the back end; requests from the front end must be answered or else the app will sit idle. Testing with real XHR requests means the front end cannot be tested until the back end is ready, a serious hindrance to parallel development.

Instead of making XHR requests, a mock can be used. Mocks are stand-in objects that can be precisely controlled for testing. A mock must implement all the functions that are used by its consumer. Luckily, the XHR mock (named AjaxMock) only has to implement a small portion of the overall jQuery.ajax. This single mocked function provides the ability to synthesize every possible back end response. Several extra public functions have been added to facilitate unit testing.

AjaxMock interface

Finish with some tests!

Now that both the harness and the XHR mock are ready, we can write some tests! The test suite consists of six distinct tests. Every test instantiates a fresh AuthenticationForm object and XHR mock. The XHR mock makes it possible to write tests for every possible back end response.

Summary

It took a while, but we got to where we want to be. Our code is easy to read, easy to re-use, and has a full test suite.

Writing testable code is often a challenge, but the basics are easy once you are used to it. Before writing a single line of code, start with the question “how am I going to test this?” This simple question will end up saving countless hours and give you confidence when refactoring or adding new features.

All code is available on Github at https://github.com/shane-tomlinson/shanetomlinson.com/tree/master/2013-jan-writing-testable-ui-javascript.

If you have any questions, send a shout.

Final product

index.html

authentication-form.js

start.js

tests/index.html

tests/ajax-mock.js

tests/authentication-form.js

  1. Thanks for your post!

    As a non-js programmer, I am at lost about the best practices of web developing. This article started to guide me on the right path.

  2. Thanks for writing up these which I feel is good for those who have not yet found their headway.
    Also your write-up makes many assumptions which you do not expound explicitly about the style you’ve adopted OO or FP. And what frameworks you’ve been orientated with. These are far more important from my POV so that the reader can know better what she is dealing with.

Leave a Comment

Post comment

What is Persona?