Advertisement
JavaScript & AJAX

Test-Driven JavaScript Development in Practice

by

TDD is an iterative development process where each iteration starts by writing a test which forms a part of the specification we are implementing. The short iterations allow for more instant feedback on the code we are writing, and bad design decisions are easier to catch. By writing the tests prior to any production code, good unit test coverage comes with the territory, but that is merely a welcome side-effect.

Republished Tutorial

Every few weeks, we revisit some of our reader's favorite posts from throughout the history of the site. This tutorial was first published in November of 2010.


Turning Development Upside-Down

In traditional programming, problems are solved by programming until a concept is fully represented in code. Ideally, the code follows some overall architectural design considerations, although in many cases, perhaps especially in the world of JavaScript, this is not the case. This style of programming solves problems by guessing at what code is required to solve them, a strategy that can easily lead to bloated and tightly coupled solutions. If there are no unit tests as well, solutions produced with this approach may even contain code that is never executed, such as error handling logic and "flexible" argument handling, or it may contain edge cases that have not been thoroughly tested, if tested at all.

Test-driven development turns the development cycle upside-down. Rather than focusing on what code is required to solve a problem, test-driven development starts by defining the goal. Unit tests forms both the specification and documentation for what actions are supported and accounted for. Granted, the goal of TDD is not testing and so there is no guarantee that it handles e.g. edge cases better. However, because each line of code is tested by a representative piece of sample code, TDD is likely to produce less excess code, and the functionality that is accounted for is likely to be more robust. Proper test-driven development ensures that a system will never contain code that is not being executed.


The Process

The test-driven development process is an iterative process where each iteration consists of the following four steps:

  • Write a test
  • Run tests, watch the new test fail
  • Make the test pass
  • Refactor to remove duplication

In each iteration, the test is the specification. Once enough production code (and no more) has been written to make the test pass, we are done, and we may refactor the code to remove duplication and/or improve the design, as long as the tests still pass.


Practical TDD: The Observer Pattern

The Observer pattern (also known as Publish/Subscribe, or simply pubsub) is a design pattern that allows us to observe the state of an object and be notified when it changes. The pattern can provide objects with powerful extension points while maintaining loose coupling.

There are two roles in The Observer - observable and observer. The observer is an object or function that will be notified when the state of the observable changes. The observable decides when to update its observers and what data to provide them with. The observable typically provides at least two public methods: pubsub, which notifies its observers of new data, and pubsub which subscribes observers to events.


The Observable Library

Test-driven development allows us to move in very small steps when needed. In this first real-world example we will start out with the tiniest of steps. As we gain confidence in our code and the process, we will gradually increase the size of our steps when circumstances allow it (i.e., the code to implement is trivial enough). Writing code in small frequent iterations will help us design our API piece-by-piece as well as help us make fewer mistakes. When mistakes occur, we will be able to fix them quickly as errors will be easy to track down when we run tests every time we add a handful lines of code.


Setting up the Environment

This example uses JsTestDriver to run tests. A setup guide is available from the official web site.

The initial project layout looks as follows:

chris@laptop:~/projects/observable $ tree
.
|-- jsTestDriver.conf
|-- src
|   `-- observable.js
`-- test
    `-- observable_test.js

The configuration file is just the minimal JsTestDriver configuration:

server: http://localhost:4224

load:
  - lib/*.js
  - test/*.js

Adding Observers

We will kick off the project by implementing a means to add observers to an object. Doing so will take us through writing the first test, watching it fail, passing it in the dirtiest possible way and finally refactoring it into something more sensible.


The First Test

The first test will attempt to add an observer by calling the addObserver method. To verify that this works, we will be blunt and assume that observable stores its observers in an array and check that the observer is the only item in that array. The test belongs in test/observable_test.js and looks like the following:

TestCase("ObservableAddObserverTest", {
  "test should store function": function () {
    var observable = new tddjs.Observable();
    var observer = function () {};

    observable.addObserver(observer);

    assertEquals(observer, observable.observers[0]);
  }
});

Running the Test and Watching it Fail

At first glance, the result of running our very first test is devastating:

Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
tddjs is not defined
      /test/observable_test.js:3

Tests failed.

Making the Test Pass

Fear not! Failure is actually a good thing: It tells us where to focus our efforts. The first serious problem is that tddjs doesn't exist. Let's add the namespace object in src/observable.js:

var tddjs = {};

Running the tests again yields a new error:

E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
tddjs.Observable is not a constructor
      /test/observable_test.js:3

Tests failed.

We can fix this new issue by adding an empty Observable constructor:

var tddjs = {};

(function () {
  function Observable() {}

  tddjs.Observable = Observable;
}());

Running the test once again brings us directly to the next problem:

E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
 observable.addObserver is not a function
      /test/observable_test.js:6

Tests failed.

Let's add the missing method.

function addObserver() {}

Observable.prototype.addObserver = addObserver;

With the method in place the test now fails in place of a missing observers array.

E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
observable.observers is undefined
      /test/observable_test.js:8

Tests failed.

As odd as it may seem, I will now define the observers array inside the pubsub method. When a test fails, TDD instructs us to do the simplest thing that could possibly work, no matter how dirty it feels. We will get the chance to review our work once the test is passing.

function addObserver(observer) {
  this.observers = [observer];
}

Success! The test now passes:

.
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (1.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms)

Refactoring

While developing the current solution we have taken the quickest possible route to a passing test. Now that the bar is green we can review the solution and perform any refactoring we deem necessary. The only rule in this last step is to keep the bar green. This means we will have to refactor in tiny steps as well, making sure we don't accidentally break anything.

The current implementation has two issues we should deal with. The test makes detailed assumptions about the implementation of Observable and the addObserver implementation is hard-coded to our test.

We will address the hard-coding first. To expose the hard-coded solution, we will augment the test to make it add two observers instead of one.

"test should store function": function () {
  var observable = new tddjs.Observable();
  var observers = [function () {}, function () {}];

  observable.addObserver(observers[0]);
  observable.addObserver(observers[1]);

  assertEquals(observers, observable.observers);
}

As expected, the test now fails. The test expects that functions added as observers should stack up like any element added to an pubsub. To achieve this, we will move the array instantiation into the constructor and simply delegate addObserver to the array method push:

function Observable() {
  this.observers = [];
}

function addObserver(observer) {
  this.observers.push(observer);
}

With this implementation in place the test passes again, proving that we have taken care of the hard-coded solution. However, the issue of accessing a public property and making wild assumptions about the implementation of Observable is still an issue. An observable pubsub should be observable by any number of objects, but it is of no interest to outsiders how or where the observable stores them. Ideally, we would like to be able to check with the observable if a certain observer is registered without groping around its insides. We make a note of the smell and move on. Later, we will come back to improve this test.


Checking for Observers

We will add another method to Observable, hasObserver, and use it to remove some of the clutter we added when implementing addObserver.


The Test

A new method starts with a new test, and the next one desired behavior for the hasObserver method.

TestCase("ObservableHasObserverTest", {
  "test should return true when has observer": function () {
    var observable = new tddjs.Observable();
    var observer = function () {};

    observable.addObserver(observer);

    assertTrue(observable.hasObserver(observer));
  }
});

We expect this test to fail in the face of a missing hasObserver, which it does.


Making the Test Pass

Again, we employ the simplest solution that could possibly pass the current test:

function hasObserver(observer) {
  return true;
}

Observable.prototype.hasObserver = hasObserver;

Even though we know this won't solve our problems in the long run, it keeps the tests green. Trying to review and refactor leaves us empty-handed as there are no obvious points where we can improve. The tests are our requirements, and currently they only require hasObserver to return true. To fix that we will introduce another test that expects hasObserver to return false for a non-existent observer, which can help force the real solution.

"test should return false when no observers": function () {
  var observable = new tddjs.Observable();

  assertFalse(observable.hasObserver(function () {}));
}

This test fails miserably, given that hasObserver always returns true, forcing us to produce the real implementation. Checking if an observer is registered is a simple matter of checking that the this.observers array contains the object originally passed to addObserver:

function hasObserver(observer) {
  return this.observers.indexOf(observer) >= 0;
}

The Array.prototype.indexOf method returns a number less than 0 if the element is not present in the array, so checking that it returns a number equal to or greater than 0 will tell us if the observer exists.


Solving Browser Incompatibilities

Running the test in more than one browser produces somewhat surprising results:

chris@laptop:~/projects/observable$ jstestdriver --tests all
...E
Total 4 tests (Passed: 3; Fails: 0; Errors: 1) (11.00 ms)
  Firefox 3.6.12 Linux: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (2.00 ms)
  Microsoft Internet Explorer 6.0 Windows: Run 2 tests \
(Passed: 1; Fails: 0; Errors 1) (0.00 ms)
    ObservableHasObserverTest.test should return true when has observer error \
(0.00 ms): Object doesn't support this property or method

Tests failed.

Internet Explorer versions 6 and 7 failed the test with their most generic of error messages: "Object doesn't support this property or method". This can indicate any number of issues:

  • we are calling a method on an object that is null
  • we are calling a method that does not exist
  • we are accessing a property that doesn't exist

Luckily, TDD-ing in tiny steps, we know that the error has to relate to the recently added call to indexOf on our observers array. As it turns out, IE 6 and 7 do not support the JavaScript 1.6 method Array.prototype.indexOf (for which we cannot really blame it, it was only recently standardized with ECMAScript 5, December 2009). At this point, we have three options:

  • Circumvent the use of Array.prototype.indexOf in hasObserver, effectively duplicating native functionality in supporting browsers.
  • Implement Array.prototype.indexOf for non-supporting browsers. Alternatively implement a helper function that provides the same functionality.
  • Use a third-party library which provides either the missing method, or a similar method.

Which one of these approaches is best suited to solve a given problem will depend on the situation – they all have their pros and cons. In the interest of keeping Observable self-contained, we will simply implement hasObserver in terms of a loop in place of the indexOf call, effectively working around the problem. Incidentally, that also seems to be the simplest thing that could possibly work at this point. Should we run into a similar situation later on, we would be advised to reconsider our decision. The updated hasObserver looks as follows:

function hasObserver(observer) {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    if (this.observers[i] == observer) {
      return true;
    }
  }

  return false;
}

Refactoring

With the bar back to green, it's time to review our progress. We now have three tests, but two of them seem strangely similar. The first test we wrote to verify the correctness of addObserver basically tests for the same things as the test we wrote to verify Refactoring . There are two key differences between the two tests: The first test has previously been declared smelly, as it directly accesses the observers array inside the observable object. The first test adds two observers, ensuring they're both added. We can now join the tests into one that verifies that all observers added to the observable are actually added:

"test should store functions": function () {
  var observable = new tddjs.Observable();
  var observers = [function () {}, function () {}];

  observable.addObserver(observers[0]);
  observable.addObserver(observers[1]);

  assertTrue(observable.hasObserver(observers[0]));
  assertTrue(observable.hasObserver(observers[1]));
}

Notifying Observers

Adding observers and checking for their existence is nice, but without the ability to notify them of interesting changes, Observable isn't very useful. It's time to implement the notify method.


Ensuring That Observers Are Called

The most important task notify performs is calling all the observers. To do this, we need some way to verify that an observer has been called after the fact. To verify that a function has been called, we can set a property on the function when it is called. To verify the test we can check if the property is set. The following test uses this concept in the first test for notify.

TestCase("ObservableNotifyTest", {
  "test should call all observers": function () {
    var observable = new tddjs.Observable();
    var observer1 = function () { observer1.called = true; };
    var observer2 = function () { observer2.called = true; };

    observable.addObserver(observer1);
    observable.addObserver(observer2);
    observable.notify();

    assertTrue(observer1.called);
    assertTrue(observer2.called);
  }
});

To pass the test we need to loop the observers array and call each function:

function notify() {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    this.observers[i]();
  }
}

Observable.prototype.notify = notify;

Passing Arguments

Currently the observers are being called, but they are not being fed any data. They know something happened - but not necessarily what. We will make notify take any number of arguments, simply passing them along to each observer:

"test should pass through arguments": function () {
  var observable = new tddjs.Observable();
  var actual;

  observable.addObserver(function () {
    actual = arguments;
  });

  observable.notify("String", 1, 32);

  assertEquals(["String", 1, 32], actual);
}

The test compares received and passed arguments by assigning the received arguments to a variable local to the test. The observer we just created is in fact a very simple manual test spy. Running the test confirms that it fails, which is not surprising as we are currently not touching the arguments inside notify.

To pass the test we can use apply when calling the observer:

function notify() {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    this.observers[i].apply(this, arguments);
  }
}

With this simple fix tests go back to green. Note that we sent in this as the first argument to apply, meaning that observers will be called with the observable as this.


Error Handling

At this point Observable is functional and we have tests that verify its behavior. However, the tests only verify that the observables behaves correctly in response to expected input. What happens if someone tries to register an object as an observer in place of a function? What happens if one of the observers blow up? Those are questions we need our tests to answer. Ensuring correct behavior in expected situations is important – that is what our objects will be doing most of the time. At least so we could hope. However, correct behavior even when the client is misbehaving is just as important to guarantee a stable and predictable system.


Adding Bogus Observers

The current implementation blindly accepts any kind of argument to addObserver. Although our implementation can use any function as an observer, it cannot handle any value. The following test expects the observable to throw an exception when attempting to add an observer which is not callable.

"test should throw for uncallable observer": function () {
  var observable = new tddjs.Observable();

  assertException(function () {
    observable.addObserver({});
  }, "TypeError");
}

By throwing an exception already when adding the observers we don't need to worry about invalid data later when we notify observers. Had we been programming by contract, we could say that a precondition for the addObserver method is that the input must be callable. The postcondition is that the observer is added to the observable and is guaranteed to be called once the observable calls notify.

The test fails, so we shift our focus to getting the bar green again as quickly as possible. Unfortunately, there is no way to fake the implementation this – throwing an exception on any call to addObserver will fail all the other tests. Luckily, the implementation is fairly trivial:

function addObserver(observer) {
  if (typeof observer != "function") {
    throw new TypeError("observer is not function");
  }

  this.observers.push(observer);
}

addObserver now checks that the observer is in fact a function before adding it to the list. Running the tests yields that sweet feeling of success: All green.


Misbehaving Observers

The observable now guarantees that any observer added through addObserver is callable. Still, notify may still fail horribly if an observer throws an exception. The next test expects all the observers to be called even if one of them throws an exception.

"test should notify all even when some fail": function () {
  var observable = new tddjs.Observable();
  var observer1 = function () { throw new Error("Oops"); };
  var observer2 = function () { observer2.called = true; };

  observable.addObserver(observer1);
  observable.addObserver(observer2);
  observable.notify();

  assertTrue(observer2.called);
}

Running the test reveals that the current implementation blows up along with the first observer, causing the second observer not to be called. In effect, notify is breaking its guarantee that it will always call all observers once they have been successfully added. To rectify the situation, the method needs to be prepared for the worst:

function notify() {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    try {
      this.observers[i].apply(this, arguments);
    } catch (e) {}
  }
}

The exception is silently discarded. It is the observer's responsibility to ensure that any errors are handled properly, the observable is simply fending off badly behaving observers.


Documenting Call Order

We have improved the robustness of the Observable module by giving it proper error handling. The module is now able to give guarantees of operation as long as it gets good input and it is able to recover should an observer fail to meet its requirements. However, the last test we added makes an assumption on undocumented features of the observable: It assumes that observers are called in the order they were added. Currently, this solution works because we used an array to implement the observers list. Should we decide to change this, however, our tests may break. So we need to decide: do we refactor the test to not assume call order, or do we simply add a test that expects call order – thereby documenting call order as a feature? Call order seems like a sensible feature, so our next test will make sure Observable keeps this behavior.

"test should call observers in the order they were added":
function () {
  var observable = new tddjs.Observable();
  var calls = [];
  var observer1 = function () { calls.push(observer1); };
  var observer2 = function () { calls.push(observer2); };
  observable.addObserver(observer1);
  observable.addObserver(observer2);

  observable.notify();

  assertEquals(observer1, calls[0]);
  assertEquals(observer2, calls[1]);
}

Since the implementation already uses an array for the observers, this test succeeds immediately.


Observing Arbitrary Objects

In static languages with classical inheritance, arbitrary objects are made observable by subclassing the Observable class. The motivation for classical inheritance in these cases comes from a desire to define the mechanics of the pattern in one place and reuse the logic across vast amounts of unrelated objects. In JavaScript, we have several options for code reuse among objects, so we need not confine ourselves to an emulation of the classical inheritance model.

In the interest of breaking free of the classical emulation that constructors provide, consider the following examples which assume that tddjs.observable is an object rather than a constructor:

Note: The tddjs.extend method is introduced elsewhere in the book and simply copies properties from one object to another.


// Creating a single observable object
var observable = Object.create(tddjs.util.observable);

// Extending a single object
tddjs.extend(newspaper, tddjs.util.observable);

// A constructor that creates observable objects
function Newspaper() {
  /* ... */
}

Newspaper.prototype = Object.create(tddjs.util.observable);

// Extending an existing prototype
tddjs.extend(Newspaper.prototype, tddjs.util.observable);

Simply implementing the observable as a single object offers a great deal of flexibility. To get there we need to refactor the existing solution to get rid of the constructor.


Making the Constructor Obsolete

To get rid of the constructor we should first refactor Observable such that the constructor doesn't do any work. Luckily, the constructor only initializes the observers array, which shouldn't be too hard to remove. All the methods on Observable.prototype access the array, so we need to make sure they can all handle the case where it hasn't been initialized. To test for this we simply need to write one test per method which calls the method in question prior to doing anything else.

As we already have tests that call addObserver and hasObserver before doing anything else, we will concentrate on the notify method. This method is only tested after addObserver has been called. Our next tests expects it to be possible to call this method prior to adding any observers.

"test should not fail if no observers": function () {
  var observable = new tddjs.Observable();

  assertNoException(function () {
    observable.notify();
  });
}

With this test in place we can empty the constructor:

function Observable() {
}

Running the tests shows that all but one is now failing, all with the same message: "this.observers is not defined". We will deal with one method at a time. First up is addObserver method:

function addObserver(observer) {
if (!this.observers) {
this.observers = [];
}

/* ... */
}

Running the tests again reveals that the updated addObserver method fixes all but the two tests which does not start by calling it. Next up, we make sure to return false directly from hasObserver if the array does not exist.

function hasObserver(observer) {
  if (!this.observers) {
    return false;
  }

  /* ... */
}

We can apply the exact same fix to notify:

function notify(observer) {
  if (!this.observers) {
    return;
  }

  /* ... */
}

Replacing the Constructor With an Object

Now that the constructor doesn't do anything it can be safely removed. We will then add all the methods directly to the tddjs.observable object, which can then be used with e.g. Object.create or tddjs.extend to create observable objects. Note that the name is no longer capitalized as it is no longer a constructor. The updated implementation follows:

(function () {
  function addObserver(observer) {
    /* ... */
  }

  function hasObserver(observer) {
    /* ... */
  }

  function notify() {
    /* ... */
  }

  tddjs.observable = {
    addObserver: addObserver,
    hasObserver: hasObserver,
    notify: notify
  };
}());

Surely, removing the constructor causes all the tests so far to break. Fixing them is easy, however. All we need to do is to replace the new statement with a call to Object.create. However, most browsers don't support Object.create yet, so we can shim it. Because the method is not possible to perfectly emulate, we will provide our own version on the tddjs object:

(function () {
  function F() {}

  tddjs.create = function (object) {
    F.prototype = object;
    return new F();
  };

  /* Observable implementation goes here ... */
}());

With the shim in place, we can update the tests in a matter that will work even in old browsers. The final test suite follows:

TestCase("ObservableAddObserverTest", {
  setUp: function () {
    this.observable = tddjs.create(tddjs.observable);
  },

  "test should store functions": function () {
    var observers = [function () {}, function () {}];

    this.observable.addObserver(observers[0]);
    this.observable.addObserver(observers[1]);

    assertTrue(this.observable.hasObserver(observers[0]));
    assertTrue(this.observable.hasObserver(observers[1]));
  }
});

TestCase("ObservableHasObserverTest", {
  setUp: function () {
    this.observable = tddjs.create(tddjs.observable);
  },

  "test should return false when no observers": function () {
    assertFalse(this.observable.hasObserver(function () {}));
  }
});

TestCase("ObservableNotifyTest", {
  setUp: function () {
    this.observable = tddjs.create(tddjs.observable);
  },

  "test should call all observers": function () {
    var observer1 = function () { observer1.called = true; };
    var observer2 = function () { observer2.called = true; };

    this.observable.addObserver(observer1);
    this.observable.addObserver(observer2);
    this.observable.notify();

    assertTrue(observer1.called);
    assertTrue(observer2.called);
  },

  "test should pass through arguments": function () {
    var actual;

    this.observable.addObserver(function () {
      actual = arguments;
    });

    this.observable.notify("String", 1, 32);

    assertEquals(["String", 1, 32], actual);
  },

  "test should throw for uncallable observer": function () {
    var observable = this.observable;

    assertException(function () {
      observable.addObserver({});
    }, "TypeError");
  },

  "test should notify all even when some fail": function () {
    var observer1 = function () { throw new Error("Oops"); };
    var observer2 = function () { observer2.called = true; };

    this.observable.addObserver(observer1);
    this.observable.addObserver(observer2);
    this.observable.notify();

    assertTrue(observer2.called);
  },

  "test should call observers in the order they were added":
  function () {
    var calls = [];
    var observer1 = function () { calls.push(observer1); };
    var observer2 = function () { calls.push(observer2); };
    this.observable.addObserver(observer1);
    this.observable.addObserver(observer2);

    this.observable.notify();

    assertEquals(observer1, calls[0]);
    assertEquals(observer2, calls[1]);
  },

  "test should not fail if no observers": function () {
    var observable = this.observable;

    assertNoException(function () {
      observable.notify();
    });
  }
});

To avoid duplicating the tddjs.create call, each test case gained a setUp method which sets up the observable for testing. The test methods has to be updated accordingly, replacing observable with this.observable.


Summary

Test-Driven JavaScript Development
Through this excerpt from the book we have had a soft introduction to Test-Driven Development with JavaScript. Of course, the API is currently limited in its capabilities, but the book expands further on it by allowing observers to observe and notify custom events, such as observable.observe("beforeLoad", myObserver).

The book also provides insight into how you can apply TDD to develop code that e.g. relies heavily on DOM manipulation and Ajax, and finally brings all the sample projects together in a fully functional browser-based chat application.

This excerpt is based on the book, 'Test-Driven JavaScript Development', authored by Christian Johansen, published by Pearson/Addison-Wesley Professional, Sept. 2010, ISBN 0321683919, Copyright 2011 Pearson Education, Inc. Refer here for a complete Table of Contents.

Related Posts