Javascript Tutorial – Design for Testability

If you’ve ever built a client/server application, you know how tedious it can be get access to a working server in order to test client functionality. With the right design, however, this is a headache we don’t have to experience. This approach was used by our team recently when developing the Facebook game, Tic-Tac-Together, and proved to be quite successful.

The root of this design involves encapsulating the communication layer into separate objects, which you should be doing anyway. Putting communication logic directly in your higher-level code is just asking for future maintenance difficulties. For Tic-Tac-Together (TTT) we needed two communication objects: one for our server and one for the Facebook API. Below is a snippet of each class with the implementations removed.

//
// Server Class
//
ttt.Server = function() {

};

// Connects to the server.
ttt.Server.prototype.connect = function(url, callback) {
  // All the logic required to connect to the TTT server.
};

// Gets a user based on their facebook ID.
ttt.Server.prototype.getUser(facebookId, callback) {
  // Sends a get user request to the server.
};

//
// Facebook class
//
ttt.Facebook = function() {

};

// Initializes the facebook interface.
ttt.Facebook.prototype.init = function(apiKey) {
  // Initialize the Facebook JS SDK.
};

Normally I’d write all of the implementation here and then wait around for a server to become available to test if anything worked. What we’re going to do instead is create two copies of each class with the exact same signature. The live version will do real communications, and the fake version will do whatever we need it to for local testing.

//
// Test Server Class
//
ttt.test.Server = function() {

};

// Connects to the server.
ttt.test.Server.prototype.connect = function(url, callback) {
  // Don’t actually need to do anything, just raise the callback.
  if(callback) {
    callback();
  }
};

// Gets a user based on their facebook ID.
ttt.test.Server.prototype.getUser(facebookId, callback) {
  var user = new ttt.model.User();
  user.name = "Test User";
  user.id = 1;

  if(callback) {
    callback(user);
  }
};

//
// Test Facebook class
//
ttt.test.Facebook = function() {

};

// Initializes the facebook interface.
ttt.test.Facebook.prototype.init = function(apiKey) {
  // Don’t actually need to do anything here.
};

The only difference between the signatures is that I put the two new classes in another namespace, test. As you can see, the test versions of these classes don’t do any actual server communication. When connecting to the server, the client is immediately notified by raising the callback. When initializing the Facebook API, the test code doesn’t even have to do anything. When requesting a user, the test class generates a user and immediately returns it to the calling code. This is a very powerful aspect of this design – when testing locally I can change the data being returned from the test objects to whatever I need in order to fully exercise the client code.

Since the live and test versions share the same signatures, code that calls them does not have to know which version is being used. When you want to toggle between local testing and live testing, simply construct the appropriate version of the class.

var server = new ttt.test.Server();  // Test version.
//var server = new ttt.Server();  // Live version.

var facebook = new ttt.test.Facebook(); // Test version.
// var facebook = new ttt.Facebook(); // Live version.

facebook.init();

server.connect(function() {
  alert(‘connected’);
});

server.getUser(10, function(user) {
  alert(‘name: ‘ + user.name);
});

As seen above, the server and facebook variables can either be the live version or the test version. The code beneath their initialization doesn’t care at all.

Having to maintain two versions of the same class has proved to be irritating, but the benefits are well worth it. If you’re using a classical inheritance scheme, like John Resig’s, you could create base classes for each communication object and extend them for the live and test versions. That approach has benefits and leads to a more flexible design – you could easily add multiple test objects and override individual functions for different behavior.

There are lots of possible variations on this design, but what you should remember is the core idea. When designing your system, include the ability to run your client code without requiring a live (and properly functioning) server. If you have any questions, feel free to drop them below. If you’re waiting for your code to compile, you should also challenge someone to a friendly game of Tic-Tac-Together.

Leave a Reply

Your email address will not be published. Required fields are marked *