Thursday 15 April 2010

Test Driven Development & Unit Testing

Before we can properly get under way, I'm going to give a brief introduction you to Test Driven Development" (TDD) and Unit Testing.

The last couple of decades have seen the rise of TDD. You may have come across project management terms like XP or Agile, but may not have looked into them. Whilst there is much more to these practices, at their core lies the testing process. This phenomenally useful approach greatly reduces debugging time, whilst also giving the programmer confidence that their code is operating as it should. It's also a lot more fun, as you're constantly setting yourself little challenges to overcome.

Whenever you write any code there are many types of test you could use, but two in particular are recommended. The first are tests that aid your development. These are written before the code they relate to and ensure that what you are about to write actually does what you want it to. The second are unit tests. These are written once you have finished writing a class and are used to give confidence that your code is behaving as it should under all conditions.

But what are the benefits? Well, when we write tests before our code it means that we only need to code the minimum amount to satisfy our requirements. It's all too easy (especially in games) to become sidetracked into adding in lots of functionality we don't need (see "Y.A.G.N.I."). TDD helps reduce this. Furthermore, any time we want to refactor (rewrite) our code we can be sure that our changes don't break anything by simply running our tests again. This encourages very short cycles (often only a couple of minutes long) in which we:

1. Write a test that fails.
2. Write just enough code to make it pass.
3. Refactor as needed.

At first this may seem slower - after all we are now writing roughly twice as much code. The key is to remember that our tests are very simple to write, whilst bugs can often be a real pain to find. They also encourage us to spend more time refactoring as we go along (rather than trying to clean up a big mess every few hours) making our code clearer and easier to maintain/edit.

Similarly, unit tests are used to cover our backs. The tests we write as we develop are often what we call "Smoke Tests". These simply demonstrate the the code works as intended in very typical cases. But what about unusual cases? These are referred to as "Boundry Tests" and can be vitally important (we'll see an example in a minute). Even if we include some boundry tests in our development process, the chances are we'll have missed some (or a lot in my case). We can also write automated tests, which supply random inputs to our methods and are set to run many iterations (say a few billion overnight). This way we gain a lot of confidence that once our code ships, it won't start randomly throwing up bugs.

As a simple example, consider Binary Search. This is a widely known recursive algorithm where, given a sorted array, we can quickly find a value or show that it is not present. The idea is simple. At each iteration we have two positions (the upper and lower bounds). If the lower bound is greater than or equal to the higher bound we stop. Otherwise we calculate their mid point and look at the value stored in the array at that point. If it's less than the target, we set the lower bound to the mid point and repeat the process. Similarly, if higher than the target, we set the upper bound to the mid point. It sounds pretty straight forward. Yet it took 12 years before a version was written that was widely believed to be bug free. Even then, the solution had a problem. It all comes down to the mid point calculation. The obvious approach is:

mid = (lower + upper) / 2;

Unfortunately, as boundry testing revealed, this can cause a problem if the sum of lower and upper is greater than the largest number representable by an int. The solution is to use:

mid = lower + ((higher - lower) / 2);

For a much deeper examination of this example (including many suggested unit tests) see Alberto Savoia's excellent chapter "Beautiful Tests" in "Beautiful Code".

The possibility that such 'invisible' bugs can creep into seemingly correct code is the best argument I can give for the use of tests.

No comments:

Post a Comment