08 November 2009

Functional Javascript test without DOM environment

When it comes to testing Javascript, it feels like the whole world is ready to use one or the other implementations of DOM environment with some sort of html fixture.

Why DOM environment? Yes most Javascript interact with DOM, let that be visual feedback, or complete state transition.

Testing outcome of visual feedback can be a subtle business especially when it's dynamic and best done by human eyes. However more often than not, it's driven by state transition (e.g. CSS class manipulation).

Principle of progressive enhancement tells me the current state of DOM should be complete and functional. Javascript should be used to enhance user experience. One of which is to achieve smooth transition between states of DOM. It's not hard to see the appeal of AHAH over AJAX / JSON approach given that such transition should be supported with or without Javascript.

Sometimes though it's natural to go with the later approach. Even then result of DOM modification from JSON should be a valid state transition. Good news is that you can go far with support of CSS without much DOM modification to provide the required visual feedback.

Quite often there's nothing special about having Javascript, as long as what's described so far holds true. There will be a thin layer of code that binds Javascript to DOM events and another layer of code that modifies DOM.

In my current project, I've been writing RhinoUnit based Javascript tests that are more functional. It's MVC styled Javascript with model being a thin wrapper around DOM and view being an entry point for an event prevention/delegation to control. Of course most of the visual presentation logic is in CSS rules. Within test, model is stubbed out with simple code that maintains its local states, and the interaction is driven through the view, and assertions are made against the internal state of stubbed out model.

I found the tests were reasonably high level that is flexible enough to support refactoring, and it supported user scenarios well.

Example of one test
function shouldUpdateModelGivenMakeIsSelected() {
  var make = {
    name : "make", 
    options : [
      {value : "", text : "Make (all)"}, 
      {value : "audi", text : "AUDI (25)", selected : true}, 
      {value : "ford", text : "FORD (15)"}]
  var model = {name : "model", options : [{value : "", text : "Model (any)"}]};

  searchForm.model.searchFilterDoms = [make, model];
  forRequest(searchForm.model.getUpdateFormUri() + "/make/audi.json").respondWith({
    make : [{value : "audi", text : "AUDI", count : 0}],
    model : [
      {value : "", text : "Model (any)", count : 0}, 
      {value : "a3", text : "A3", count : 15}, 
      {value : "a4", text : "A4", count : 10}]

  var makeFilter = searchForm.model.getSearchFilter(make);
  assert.that(makeFilter.isSelected(), eq(true));
  assert.that(makeFilter.isRestricted(), eq(false));
  assert.that(makeFilter.getValue(), eq("audi"));
  assert.that(join(makeFilter.getOptions(), "text"), eq("AUDI"));
  assert.that(join(makeFilter.getOptions(), "value"), eq("audi"));

  var modelFilter = searchForm.model.getSearchFilter(model);
  assert.that(modelFilter.isSelected(), eq(false));
  assert.that(modelFilter.isRestricted(), eq(false));
  assert.that(modelFilter.getValue(), eq(""));
  assert.that(join(modelFilter.getOptions(), "text"), eq("Model (any)|A3 (15)|A4 (10)"));
  assert.that(join(modelFilter.getOptions(), "value"), eq("|a3|a4"));