TDD or not TDD

One of the obvious disadvantages of a short, but very intensive and wide-ranging, training courses like CodeClan is that you don’t get much time to explore topics in detail. TDD is introduced and used but the problems of access to external data are not discussed in any depth.

You can only get so far using Mocha to test the logic of your JavaScript before you need to access a database and, in all probability, that database will require a network connection. You could start at the bottom of the stack, testing each layer before adding the layer above but it becomes increasingly hard to force specific events to occur. This is integration testing rather than unit testing.

So what do you do when you’re limited for time and your test framework doesn’t provide the support you need to isolate the units in your code? You can either take the time out to determine how to implement TDD or plough on using a combination of integration and ad-hoc tests. Using the former approach you will have more confidence in the code you have written but you risk running out of hours to do the job while the latter approach may result in a fully working solution but contain hidden technical debt.

When I left CodeClan, I gave myself a few weeks to implement a Cordova app based on code produced during my time at CodeClan. As you might guess, a lack of time and appreciation of the available tools had meant the original code made poor use of TDD and I was determined to have a working app. There was no time to go back and do things properly so I decided to plough on. The app was completed on time and works well but I was worried. I had learned a lot but I had skipped over a very important subject so I decided to go back and write the tests.

While not the TDD way, writing the tests after the fact is proving to be an interesting experience. I have often heard people say that TDD causes you to write code differently and that becomes obvious very quickly when you try and work out how to disconnect units. I like to think that I have chosen my classes well and followed SOLID principles so the coupling between units is low enough but it took me a while to find the right tools for the job.

“Dependency Injection” and the “Inversion of Control” are two very grand titles for the concepts that underpin Unit Testing and they are explored in great depth in too many places for me to mention. However, in practice all this means is that you have to be able to give the Unit under test the tools it needs to access data from the test environment. In this way you can replace a database (or anything else) by supplying a new data-store that uses an approach of your choice.

I chose the Sinon JS tool to support my test imitative because it is well recommended and widely deployed. After a ton of reading I decided I needed to use a stub. Then I had to work out where to put it; somehow I needed to stub out the call to the network without changing anything else. Here’s a bit of code that called remote database to get a list of mountains.

Mountains.prototype.fetchMountains = function(onCompleted) {
  const url = baseURL + "munros";
  const apiRequest = new ApiRequest();
  apiRequest.get(url, function(rxMountains) {
    this._mountains = rxMountains;
    this._saveToStore(rxMountains);
    onCompleted(rxMountains);
  }.bind(this))
}

After some head-scratching it became obvious that I needed to stub out the apiRequest.get() without losing the code in the callback so this is how I reorganised the code. The function _fetch() can now be stubbed out without losing any functionality.

Mountains.prototype._fetch = function(resource, onCompleted) {
  const url = baseURL + resource;
  const apiRequest = new ApiRequest();
  apiRequest.get(url, function(rxResource) {
      onCompleted(rxResource);
  });
}

Mountains.prototype.fetchMountains = function(onCompleted) {
  this._fetch("munros", function(rxMountains) {
    this._mountains = rxMountains;
    this._saveToStore(rxMountains);
    onCompleted(rxMountains);
  }.bind(this)) 
}

So now the Dependency Injection is done in the test like this:

// Replace _fetch() with a stub
let stub = sinon.stub(mountains, "_fetch");
// When called with arg "munros" return dummy data stub_munros
stub.withArgs("munros").yields(stub_munros);
// Call the method to retrieve the dummy data
mountains.all(function() {});
// Remove the stub before the asserts in case something fails
mountains._fetch.restore();
// Check the stub was called
assert.strictEqual(stub.callCount, 1);
assert.strictEqual(mountains.updateInterval, 0);

Clearly this is not a generic DI mechanism but it will work very well for tests. I wish I had known about this a long time ago! It took a while to work out what I needed to do but using Sinon spies and stubs has been painless.

There are some good examples on the Enterprise JS Blog. I hope you find this useful.

Leave a Reply

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