Writing testable code

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn

As written in The Mythical Man Month you either plan half of your schedule for testing or spend twice that debugging.

 

However writing tests is not easy. Mocking strategies and full system tests allow one to easily orchestrate the parts of the application that cannot be unit tested, but they come at a significant increase in complexity and cost, especially when you need the full application stack up and running to test a couple lines of code.

Integration tests are slow and costly
Integration tests are slow and costly to develop and to run. Do you really need so many of them?

Wouldn’t it be better just to write testable code in the first place?

 

Don’t get me wrong, I like Selenium even if I hate testing for browser compatibility, but it is just so slow and cumbersome where unit testing would get you much further in real, useful test coverage with less effort.

Let’s consider this snippet of code:

//display groceries on a list
$.getJSON('groceries.json', function(data) {
  var ul = $('#parent').append('<ul></ul>').find('ul');
  for (var idx = 0; idx < data.length; ++idx) {
    data[idx] = data[idx].charAt(0).toUpperCase() +
    data[idx].slice(1).toLowerCase();
  
    var li = ul.append('<li></li>').find('li').last();
    li.text(data[idx]);
  }
 
})

How would a unit test of this work, short of using either Selenium – or another full browser stack – plus all the infrastructure necessary for the requests to run? The closest injection point is to mock up that groceries.json network call, but then there is also an implicit dependency on being in a web page as well, because of the inline DOM manipulation.

Compare with this variation:

//normalize string case
var process = function(data) {
  for (var idx = 0; idx < data.length; ++idx) {
    data[idx] = data[idx].charAt(0).toUpperCase() +
    data[idx].slice(1).toLowerCase();
  }
  return data;
}


//create a html list out of a data list
var display = function(data) {
  var ul = $('#parent').append('<ul></ul>').find('ul');
  for (var idx = 0; idx < data.length; ++idx) {
    var li = ul.append('<li></li>').find('li').last();
    li.text(data[idx]);
  }
}

//display groceries on a list
$.getJSON('groceries.json', function(data) {
    data = process(data);
    display(data);
  }
 
})

Just ignore for a second that the forEach() method could chain the two functions better, as I arranged the method this way for the sake of simplicity.

This snippet does more or less the same thing, except it is slightly slower.

In this updated code the process function can be easily tested and verified to work over a range of inputs, by using a JavaScript interpreter such as Rhino.

By using some browser emulator such as env.js, we can even unit test the display function, by fetching a empty page running the code and verifying the results. I used that approach on the IBM Social Business Toolkit to run headless, networkless unit tests during the maven build itself with great results.

On the Social business Toolkit I even got as far as to mock the dōjō toolkit xhr object and serve requests out of XML files, but that was kind of a specific use case and probably worthy of a followup, separate article.

In general, if you take a step back and look at code as the data flows in it, it takes this general form:

codeflow
Data flows in a given method

Input and output are easy to mock and to verify, they follow the code flow and necessitate very little effort to test.

Memory access is slightly more complex: if an object requires to be in a certain state or needs access to certain global object, you will either need some initialization code or mocked implementer for allowing the test to feed the objects arbitrary data as needed.

Access to external data in unit tests is the most cumbersome and costly part to validate and mock: all the operations that interact with an external systems such as databases, screens, remote API and such are difficult to mock in a test due to their nature and even more difficult to use live in an integration test, given their lifecycle is separated from the tested application.

However, a couple of patterns here can help: inversion of control and locator objects make it easy to convert a global memory access, like a singleton, into something much more manageable:

//static access
public void doSomething() {
  PrintManager.getSingleton().print(this.data);
}

//locator 
public void doSomething() {
 ServiceLoader.load(PrintManager.class).print(this.data)
}

//inversion of control
public void doSomething(PrintManager p) {
 p.print(this.data);
}


With static access, your only recourse is to mock the (unseen) PrintManagerImplementation object.

With the locator, you can provide an implementation to the PrintManager interface that can throw exceptions or verify the correctness of the input data more easily, even if it means maintaining a list of implementer and test separately in the test for the loader. It is, however, the less invasive method to apply to existing code.

Inversion of control is more straightforward: the interface is given to the object by external means, and the test can pass it’s own implementation to be used by the component. This is by far the best and simplest method but it does require slight modification to existing code and cannot be pushed too far, as at some point someone has to be in control.

 

These patterns slightly help in making the code testable, but we want it to be easily testable as well.

As a general strategy, constraining the data flow itself to fewer paths enables easier testing. The best case happens when in a given method you can limit the data flow to a single-input/single-output pattern:

 

samples
Constraining data flows allow for easier unit testing.

In less abstract terms, the constraining of data flows is something already ritualized in many patterns: one can imagine the flow exemplified in the above image to be a facade, a strategy, a simple setter and a DAO.

 

By splitting methods, encapsulating access to resources and strategical use of inversion of control the code can be made much easier to test. But it also impacts how easier it is to compose and restructure the code later on. All the simpler pieces, as opposed to monolithic code blocks, are far easier to refactor in new, interesting ways when business requirements change.

Orchestrating all these components might be harder, but as long as your orchestrator is dumb the need of testing it is limited. What the objective of refactoring for testability should be is thus pushing complexity down on methods as far as convenient to either reduce data flow paths or reduce control flows decision on any given point of code.

//calculate taxes and shows them to the user
var showTaxRate = function() {
  var totalPrice = globalCart.getTotalPrice();
  var rate;
  if (user.isDutyFree()) {
    rate = 0;
  } else {
    var country = currentUser.getCountry();
    rate = CountryTaxSpec.getRateFor(country);
    $('#tax').text(totalPrice * rate);
  }
};

 

Compare with:

var DutyFreeStrategy = {
    apply: function(price) { return 0; }
};
...
// decide which strategy use for calculating taxes
var TaxStrategy = {
  forUser: function(user) { 
    if (user.isDutyFree())
      return DutyFreeStrategy;
    else
      return DefaultVatStrategy;
  }
};
//calculate taxes
var calculateTaxRate = function(totalPrice, user) {
 var strategy = TaxStrategy.forUser(user);
 return strategy.apply(totalPrice);
};


Removing the if/else control flow and pushing out the data flow to the DOM model makes this function much easier to test: the test is pushed away into the strategy object and there is no dependency on the globalCart object being initialized or valid at any point. Simplifying the code to the point it is trivial to test is the best way to reduce testing cost.

 

Code has to be tested anyway, and there is no such excuse as ‘code being too complex’: if it truly is, then that’s also the point where the most bug will be anyway, so please make writing tests actually possible!

 

 

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn

Leave a Reply

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