Writing Testable Frontend Javascript Part 1 - Anti-patterns and their fixes

This is the first of a two part introduction on how to write testable Javascript UI code. The first article presents a sample application that contains several common anti-patterns and how these can be refactored to be more testable.

The complete refactor is presented in the second article along with info on how to add a unit test suite and make use of mocks.

Front end development comes with a set of challenges that are rarely discussed in articles about unit testing. Self initialization, encapsulated logic, DOM event handlers, XHR requests, and nested callbacks all make testing difficult.

Fortunately, writing front end code so that it can be tested is straight forward, but it does require a little knowledge and thought.

Common coding practice - Easy to understand, difficult to test

While short, this contrived example uses several common anti-patterns.

<!DOCTYPE html>  
<html>  
<head>  
  <title>An Untestable Authentication Form</title>
</head>  
<body>  
  <form id="authentication_form">
    <label for="username">Username:</label>
    <input type="text" id="username" name="username" />
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" />
    <button>Submit</button>

    <p id="username_password_required" style="display: none;">
      Both the username and password are required.
    </p>

    <p id="authentication_success" style="display: none;">
      You have successfully authenticated!
    </p>

    <p id="authentication_failure" style="display: none;">
      This username/password combination is not correct.
    </p>

    <p id="authentication_error" style="display: none;">
      There was a problem authenticating the user,
      please try again later.
    </p>
  </form>
  <script src="jquery.min.js"></script>
  <!-- Inline Javascript is impossible to test 
         from an external test harness -->
  <script>
    // Even if test harness was included in the 
    // HTML, Javascript is inaccessible to tests
    $(function() {

      // Pyramid of doom - A mixture of 
      // disparate concerns and very difficult 
      // to test individual parts
      $("#authentication_form").on("submit",
                                  function(event) {
        // Event handler logic is mixed with 
        // form handling logic
        event.preventDefault();

        var username = $("#username").val();
        var password = $("#password").val();

        if (username && password) {
          // Without a mock, XHR requests require 
          // a functioning back end, adding extra
          // dependencies and delay
          $.ajax({
            type: "POST",
            url: "/authenticate_user",
            data: {
              username: username,
              password: password
            },
            success: function(data, status, jqXHR) {
              // Knowing when this completes 
              // requires some sort of notification
              if (data.success) {
                $("#authentication_success").show();
              }
              else {
                $("#authentication_failure").show();
              }
            },
            error: function(jqXHR, textStatus,
                                        errorThrown) {
              $("#authentication_error").show();
            }
          });
        }
        else {
          $("#username_password_required").show();
        }
      });
    });
  </script>
</body>  
</html>  

Anti-patterns that make this application difficult to test

  • Inline Javascript - Javascript that is embedded in an HTML file is impossible to include in an external unit test harness.
  • Inaccessible code - Even if the Javascript was externalized, no public interface is provided.
  • Missing constructor/prototype object - Individual unit tests are meant to operate in isolation. Testing a singleton is difficult because the results of one test could affect the results of another.
  • Pyramid of doom - Deeply nested callbacks are rampant in Javascript development, but they scream of a mixture of concerns. Logic inside the pyramid is difficult to test in isolation, and over time, have a tendency to turn into unmaintainable spaghetti.
  • Poorly constructed DOM event handlers - Event handler logic is mixed with form submission logic, resulting in an inflexible mixture of concerns.
  • Real XHR Requests - Real XHR requests require an available back end. Rapid parallel development of both the front and back ends is difficult since XHR requests require a working back end.
  • Notification-less asynchronous logic - Without some sort of notification, it is impossible to know when an asynchronous function has completed.

How To Write Testable Javascript UI Code

Each of the outlined problems can be addressed. With a little thought, front end code is easy to test.

Externalize all Javascript

Javascript that is directly embedded into an HTML file is impossible to include into another HTML file. External Javascript is ready for re-use and can be included into more than one HTML file.

Provide a public interface

Code must have a public interface for it to be tested. The Module is the most commonly used pattern to encapsulate logic while providing a public interface. In his excellent book Essential Javascript Design Patterns, Addy Osmani states that "the Module pattern was originally defined as a way to provide both private and public encapsulation for classes in conventional software engineering."

The original sample application has no public interface; all code is encapsulated inside of a self-invoking function. The only hook that can be used for testing is the submit event. While certainly possible, writing tests using only synthetic events is unnecessarily difficult.

A properly encapsulated Module can be used to limit access to functions, reduce pollution of the global namespace, and provide a public interface from which to test.

var PublicModule = (function() {  
  "use strict";

  // This is the public interface of the Module.
  var Module = {
    // publicFunction can be called externally
    publicFunction: function() {
      return "publicFunction can be invoked "          
                  + "externally but " 
                  + privateFunction();
     }
  };

  // privateFunction is completely hidden
  // from the outside.
  function privateFunction() {
     return "privateFunction cannot";
  }

  return Module;
}());

As Addy points out, a drawback to the Module pattern is "the inability to create automated unit tests for private members." A function that is not directly accessible cannot be directly tested. A tension exists in module design between keeping members private and exposing members to the public.

In the Mozilla Persona code base, we frequently expose difficult to test private functions to the public interface, clearly marking the extra functions as part of a test API. While other developers are still able to call these private functions, the author's intentions are clear.

publicFunction: function() {  
   return "publicFunction can be invoked "
        + "externally but " 
        + privateFunction();
}

// BEGIN TESTING API
,
privateFunction: privateFunction  
// END TESTING API
};

// privateFunction is now accessible 
// via the TESTING API
function privateFunction() {  
...

Code between the // BEGIN TESTING API and // END TESTING API pseudo-markers can be removed for production during the build process.

Use Instantiable Objects

The original application does not use instantiable objects, its code is designed to be run only once. This constraint makes it difficult to reset state and run unit tests independently of each other.

It is easier to test modules that can be instantiated multiple times. In Javascript, two similar approaches exist: a constructor function and Object.create.

Since PublicModule two examples ago is an object and not a function, Object.create is used to create duplicate objects. An optional init function can be added to the prototype to perform initialization that is traditionally done in a constructor.

...
// the init function takes care of initialization 
// traditionally done in a constructor
init: function(options) {  
  this.valueSetOnInit = options.valueSetOnInit;
},

publicFunction: function() {  
...
// create an instance of the PublicModule.
var objInstance = Object.create(PublicModule);  
objInstance.init({  
  valueSetOnInit: "value set during initialization"
});

Flatten the pyramid of doom

Deeply nested callbacks have unfortunately become a staple of front end Javascript programming. The Untestable Authentication Form example is by no means extraordinary with callbacks nested three deep. Deeply nested callbacks are a code smell - they stink of poorly composed functionality and a mixture of concerns.

Breaking the pyramid into constituent components results in flat code that is made up of small, cohesive, and easy to test functions.

Separate DOM event handlers from the action it performs

The Untestable Authentication Form uses a single submit handler to take care of both event processing and form submission. Not only are two concerns taken care of, but this mixture results in an inability to submit the form programmatically without using a synthetic event.

$("form").on("submit", function(event) {
  event.preventDefault();

  // this code is impossible to invoke
  // programmatically without using a 
  // synthetic DOM event.
  var name = $("#name").val();
  doSomethingWithName(name);
});

Separating the form processing logic from the event handling logic allows the form to be submitted programmatically without resorting to synthetic events.

$("form").on("submit", submitHandler);

function submitHandler(event) {  
  event.preventDefault();

  submitForm();
});


// form submission can now be done programmatically
// by calling submitForm directly.
function submitForm() {  
  var name = $("#name").val();
  doSomethingWithName(name);
}

Unit tests can call submitForm instead of using a synthetic submit event.

Mock in XHR requests

Almost all modern sites use XHR (AJAX) requests. 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.

// This is an explicit dependency on the jQuery ajax
// functionality as well as a working back end.
$.ajax({
  type: "POST",
  url: "/authenticate_user",
  data: {
    username: username,
    password: password
  },
  success: function(data, status, jqXHR) {

Instead of making actual XHR requests, use an XHR mock that responds in a well defined manner. Mock objects "are simulated objects that mimic the behavior of real objects in controlled ways." Mocks are useful in situations where a dependency contains functionality that is unavailable, slow, cannot be controlled, or is too buggy to rely upon. XHR requests are just one example.

Testing both front and back ends together is important, but this is better left to functional tests. Unit tests are meant to test items in isolation.

A module that makes an XHR request should accept an XHR mock that is injected into its constructor or init function. The module then uses the injected XHR object instead of a direct call to $.ajax. A mock XHR object is injected while running unit tests whereas modules running in production use $.ajax.

Sensible defaults can be declared to reduce the amount of initialization code needed for production systems.

init: function(options) {  
  // Use the injected ajax function if 
  // available, otherwise use $.ajax by default.
  this.ajax = options.ajax || $.ajax;
},

submitForm: function() {  
  ...
  // This can call either an XHR mock or 
  // a production XHR resource depending 
  // on how the object is initialized.
  this.ajax({
    type: "POST",
    url: "/authenticate_user",
      data: {
        username: username,
        password: password
    },
    ...
  });
}

Asynchronous programming requires notifications

The Untestable Authentication Form example lacks a notification mechanism to indicate when all processing has completed. This is a problem for unit tests that run after an asynchronous function has finished.

Many notification mechanisms exist in Javascript: callbacks, observables, and events are just a few. The simple callback is by far the most common.

submitForm: function(done) {  
  this.ajax({
    ...
    // an ajax call is asynchronous. When it
    // successfully completes, done is called.
    success: done
  });
}  

Clean up after yourself

Unit tests should run in isolation of each other; once a test completes, all state should be destroyed, including DOM event handlers. Two tests that both cause objects to bind DOM events handlers to the same DOM element have a good chance of inadvertently interfering with each other. To eliminate this interference, a dead object should remove all of its DOM event handlers. The extra work comes with an additional advantage; memory leaks are greatly reduced in applications that create and destroy many objects.

teardown: teardown() {  
  $("form").off("submit", submitHandler);
}

Summary

That's it. There really isn't much to writing front end Javascript code so that it can be tested. A public interface, instantiable objects, a flat code structure, well composed event handlers, and a way to clean up after yourself.

Code for this article can be found on Github.

Part 2 of this series uses the concepts introduced here to refactor the sample application, write a simple XHR mock, and add a full unit test suite.