Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Tips for writing testable, maintainable page-specific javascript

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.

Comments
  1. Dan Pickett says:

    Awesome stuff – I use a few Of these techniques myself and agree with a few of these points. How do you feel about a more object oriented approach for wrapping both API and Dom behavior? I often write jquery functions so I can properly enclose behavior. Ie, in your date picker example above, I might have $(“input.date picker”).pivotalDatePicker(). I find it helps in making code more testator and extensible.

  2. Jeff Dean says:

    I’ve see extensions like this often, and I like the way they look, but I tend to avoid them.

    Even though the jQuery method that allows for such extensions is called `extend`, you are modifying the framework objects directly, which doesn’t follow the open/closed principle (open for extension, closed for modification).

    To me this seems akin to monkey-patching methods on to objects in Rails. As you pointed out, you probably want to prefix your methods with a namespace (_pivotal_DatePicker). Whenever I see a group of methods with a common prefix, I think about refactoring them to a class, so that:

    $.fn.extend({
    pivotalDatePicker : function(){},
    pivotalTextAreaResizer : function(){},
    });

    becomes

    var Pivotal = {
    datePicker : function(){},
    textAreaResizer : function(){},
    };

  3. Dan Pickett says:

    I fail to see how it violates the open/closed principle. You’re essentially just specifying a higher order abstraction/a wrapper to prevent duplication.


    $(function(){
    $("input.date_picker").pivotalDatePicker();
    });

    jQuery.fn.pivotalDatePicker = function(){
    return this.each(function(){
    new PivotalDatePicker(this);
    });

    function PivotalDatePicker(element, options){
    //you can specify some defaults here, customizations etc
    $(element).datePicker();
    }
    }

  4. Dan Pickett says:

    sorry markdown swallowed my example:

    $(function(){
    $(“input.date_picker”).pivotalDatePicker();
    });

    jQuery.fn.pivotalDatePicker = function(){
    return this.each(function(){
    new PivotalDatePicker(this);
    });

    function PivotalDatePicker(element, options){
    //you can specify some defaults here, customizations etc
    $(element).datePicker();
    }
    }

  5. Jeff Dean says:

    My understanding of open/closed is that you try to avoid modifying existing classes. Let’s say jQuery.fn has 10 functions to start with. In your example you added a function to jQuery.fn, so jQuery.fn has 11 functions, which means you’ve modified the object.

    Practically speaking, let’s say you want to convert your site to Prototype (not that I’ve ever seen that happen) and you decide to do it incrementally. In your example you’d have to either change all of the callers of the date picker or add similar functionality to Prototype objects.

    If instead of modifying the jQuery.fn class, you use unrelated classes, like:

    var Pivotal = {
    datePicker : function(){
    // code here
    },
    };

    you can just change the implementation of that method and you’re done.

  6. Dan Pickett says:

    Fair point – I guess I view JQuery more as a framework than a closed system. I equivocate adding a JQuery function with adding a model or controller in Rails. To me, it is a more permanent design decision than a class you wrote of which you might refactor later.

    If you change your framework, you’re going to have a lot of work to do anyhow. You solution is definitely much more framework agnostic, but you’ll likely have internal changes to make anyhow when refactoring from JQuery to Prototype.

    Great post and thanks for the discourse afterwards! As developers, I think we tend to focus on server side programming and javascript really doesn’t get the attention it deserves. Looking forward to reading more.

  7. Jack Gordon says:

    Good tips.

    One question, in your “separate behavior and wiring” example, you do this:

    $(document).live(“content:loaded”, …)

    Seems like using live is unnecessary in this case since you won’t really be adding any document objects to the page after the initial load so no need to use event delegation on the document object.

    I wouldn’t point it out except I think it dilutes the purpose of live for people that might not be familiar with it’s intended use.

    Thanks!

  8. Jeff Dean says:

    That’s a fair point, Jack. Using `bind` would work there as well, and be simpler. Thanks!

  9. Justin H Johnson says:

    Great article. Consider taking the idea of custom events a step further to decouple functions (namespaced, object literal ones, ideally) from one another. A simple use case would be injecting analytics into your site. When a user interaction occurs, trigger an event instead of firing a specific analytics function:

    ex.global.js:

    var Site = Site || {};
    Site.SomePage = (function() {
    var self = {
    "init": function() {
    self.doAnimation();
    },
    "doAnimation": function() {
    //do something

    //done
    $(document).trigger("endAnimation", "pageName");
    } //end queue
    };
    return self;
    })();

    ex.analytics.js:

    var Site = Site || {};
    Site.Analytics = (function() {
    var self = {
    "init": function() {
    //Add event listeners
    $(document).bind("endAnimation", function(e, page) {
    switch (page) {
    case "pageName":
    //set some page var
    break;
    default:
    break;
    }
    //fire your analytics code
    });
    }
    };
    return self;
    })();

    This way you can have all sorts of loosely coupled callback methods for a single function.

  10. Justin says:

    Markdown fail.

    ex.global.js:
    var Site = Site || {};
    Site.SomePage = (function() {
    var self = {
    “init”: function() {
    self.doAnimation();
    },
    “doAnimation”: function() {
    //do something

    //done, let the world know about it
    $(document).trigger(“endAnimation”, “pageName”);
    } //end queue
    };
    return self;
    })();

    ex.analytics.js:
    var Site = Site || {};
    Site.Analytics = (function() {
    var self = {
    “init”: function() {
    //Add event listeners
    $(document).bind(“endAnimation”, function(e, page) {
    switch (page) {
    case “pageName”:
    //set some page var
    break;
    default:
    break;
    }
    //fire your analytics code
    });
    }
    };
    return self;
    })();

  11. Great article, especially as I’ve been using Jasmine lately and it’s nice to see some other real-world examples. Writing testable JavaScript code — especially with the idioms common to jQuery — seems to be a real pain point for people, so it’s great to get more information out there about how to do it well.

    There were a couple of things in the post that I wanted to bring up, more as points of discussion than as issues per se.

    First, I wanted to call people’s attention to the jQuery UI widget factory for creating stateful widgets, which is what it seems like you’re doing by marking the datepicker element with the processed class. Classes are one way to indicate state, but for anything remotely complex, the jQuery UI widget factory can be a good choice — and perfectly testable, too.

    Second, I’m not sure I 100% agree that it never makes sense to make facades for core framework functions. The example you used is, admittedly, questionable, but I think it can make a great deal of sense to create a facade for a set of related server-side services, such that they can be accessed via a simple API. So, for example

    blogPosts.get(‘5′, callbackFn);

    might trigger an XHR, but the implementation is irrelevant to the consumer of the API. This has benefits as far as keeping code DRY, and also in terms of allowing changes to implementation without impacting the code that consumes the service. I’d even argue it’s more testable than having $.ajax() calls throughout application code.

  12. Jeff Dean says:

    @rebecca – thanks for pointing out the jQuery UI widget factory – I hadn’t seen that before.

    And I think we’re in agreement about facades. I don’t think the code in your example is really a facade, as much as just an example of your domain objects. Here’s one hypothetical implementation of the `blogPost` domain object:

    var blogPost = {
    get : function(id) {
    var url = MyApp.routeFor(‘blog’, id);
    $.get(url, function(data){
    $(document).trigger(“blog:get”, data)
    });
    }
    }

    In this, you use `$.get` directly. You haven’t added facades around `$.get` or `trigger`. But take another hypothetical example:

    var blogPost = {
    get : function(id) {
    var url = routeFor(‘blog’, id);
    MyApp.get(url, function(data){
    MyApp.trigger(“blog:get”, data);
    });
    }
    }

    Here there are facades around `get` and `trigger`. I’ve seen this several times and so far it has always led to a decrease in productivity for me.

    In my experience frameworks tend to be very well tested, elegant and rarely need app developers to re-implement core behavior (like `$.ajax`, `live`, `bind` etc…)

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *