Tips for writing testable, maintainable page-specific javascript

December 3, 2010 Pivotal Labs

I’ve worked on several large Rails apps and seen at least a dozen javascript systems. In this post I’ll describe a few techniques that I’ve seen that consistently make javascript easier to test and maintain. To those of you who write javascript more than I do, these might be old news, but it’s taken me 4 years to learn them!

Add your own document ready method

Let’s say you have a page where you need to add a date picker widget to a text field. Let’s also say that you decide not to write a javascript unit test for this file. You follow the example on the widget’s site and come up with something like this:

$(document).ready(function(){
  $(".date_picker").datePicker();
})

It works, and life is good. Over time the requirements change, and you have to pass in some complex functions to see which dates should be highlighted in the widget so you decide to write a Jasmine test:

describe("adding a date picker", function(){
  it("should add a date picker", function(){
    $("#jasmine_content").html('<input class="date_picker"/>');
    expect($('.date_picker.processed').length).toEqual(1);
  });
});

After watching it fail several times, you try to figure out what’s happening. Then it dawns on you – $(document).ready() fired when the dom was loaded, and then you added more elements to the dom, so they didn’t get whatever happened on document ready.

If instead you create your own custom event on document ready, and then listen for that event in all of your custom code you can avoid this problem. For example:

$(document).ready(function(){
  $(document).trigger("content:loaded")
});

$(document).live("content:loaded", function(){
  $(".date_picker").datePicker();
});

Since you are listening for a custom event, you can trigger that custom event in your javascript specs:

describe("adding a date picker", function(){
  it("should add a date picker", function(){
    $("#jasmine_content").html('<input class="date_picker"/>');
    $(document).trigger("content:loaded");
    expect($('.date_picker.processed').length).toEqual(1);
  });
});

This has the added benefit of giving you an easy way applying the same date picker to inputs that are dynamically added to the page later on.

(thank you Evan Farrar for introducing me to this)

Separate behavior and wiring

The example above is a bit more testable, but in your test since you are firing an event that’s meant to be global, you may still end up getting more code executed than you bargained for. In addition, it’s a pretty high-level test, which often leads to needing lots of setup. To avoid that, you can separate your code from the code that applies it to the page. For example:

var DatePicker = {
  setup : function() {
    $(".date_picker").datePicker();
  }
}

$(document).live("content:loaded", function(){
  DatePicker.setup();
});

Now the spec looks a lot more like unit tests you would write in any other language:

describe("DatePicker#setup", function(){
  it("should add a date picker", function(){
    $("#jasmine_content").html('<input class="date_picker"/>');
    DatePicker.setup();
    expect($('.date_picker.processed').length).toEqual(1);
  });
});

To test that the code is being wired up correctly, all you need to do is write a single spec that spies on DatePicker.setup() then triggers content:loaded. Separating code from wiring makes it easier to extract common code to different javascript files, since the classes that apply behavior are standalone.

(thank you to Rajan Agaskar for introducing me to this)

Make all behaviors idempotent

Taking the example above, let’s say that you dynamically create a textbox on the page after page load and need to apply the date picker to it. After adding the input element you trigger your custom event content:loaded and notice that 2 date pickers now appear on the first input! Oops.

You notice that your date picker widget adds a class called “processed” to each input that it applies to, so you update your DatePicker to ignore these:

var DatePicker = {
  setup : function() {
    $(".date_picker:not(.processed)").datePicker();
  }
}

(thanks to Corey Innis for introducing me to this technique)

Add facades for smaller, non-framework third-party libraries

When working with 3rd party libraries it’s normally a good idea to create a facade for your app so that you can swap out the 3rd party library without changing the code that references it. Whenever I work with 3rd party libraries, especially things like API clients (facebook, twitter, bit.ly) and most gui widgets (date pickers, menus) I like to write facades.

For example, let’s say you use the bit.ly api, which looks something like this:

BitlyClient.call('shorten', {'longUrl': url}, 'Some.function');

A simple facade for this service might look like this:

var UrlShortener = {
  shorten : function(url){
    BitlyClient.call('shorten', {'longUrl': url}, 'UrlShortener.shortened');
  },

  shortened : function(data){
    $(document).trigger("url:shortened", [data])
  }
}

With this, you can easily call UrlShortener.shorten and listen for links that have been shortened, so it’s a bit easier to test and to mock out in tests.

(thanks to Ben Stein for introducing me to facades for API libraries)

Don’t add facades for core framework functions

I make an exception to this when it comes to framework code, such as Rails or jQuery. For some reason, I’ve seen people consistently re-implement event listening behavior like so:

var MyApp = {
  register : function(selector, event, callback) {
    $(selector).live(event, callback);
  }
}

Instead of calling jQuery live in code, you call MyApp.register. I’ve rarely seen the benefit of adding abstraction layers to framework code, but I have experienced the productivity loss of having to learn facades that sit on top of frameworks, and dealing with bugs in those implementations. I recommend just using frameworks like jQuery and Prototype directly.

Don’t create a facade for Google Maps

Google Maps is not a framework, so in theory creating a facade would be a good idea. In practice however, I haven’t seen it work. Unless you are using extremely basic map functionality, you will likely not find feature parity across the major javascript map providers like Yahoo! and Microsoft, so if you have to change providers it’s going to be painful anyway, and adding layers of abstraction will be counter productive.

Summary

In my experience, if you trigger behavior with custom events, separate your code from the wiring and write idempotent behaviors, most of your code will exist behind simple, well-tested objects, and refactoring will be easy. Adding facades for small third party libraries, or third party libraries that are likely to change often, can make it easier to test and maintain your code. By avoiding unnecessary facades for large, common libraries like Google Maps and jQuery you can reduce the number of API’s a developer needs to know without affecting the time it takes to switch should you need to.

About the Author

Biography

More Content by Pivotal Labs
Previous
New in Tracker: Updated look, Google Accounts sign-in
New in Tracker: Updated look, Google Accounts sign-in

Tracker has a new look! The Tracker team has been busy since the hosting move last month. We've shifted ou...

Next
Maintainable State Machines Part 2 – don’t store state names in the database
Maintainable State Machines Part 2 – don’t store state names in the database

In relational databases it's common to use foreign keys to reference other tables so that when you make a c...

Enter curious. Exit smarter.

Register Now