An Argument for Test Driven Development

The concept of Test Driven Development (TDD) can seem foreign to many managers and even developers.  The conventional thinking has been to test your code once it’s working and when you think it’s complete. TDD states that tests must be written before you start to develop and let the tests guide your development.

This was odd to me at first.  How do you test something that isn’t there? I figured this fad would die soon.

I was wrong, not only about its life-span, but its facility. Tests should check if your code does what it was *intended* to do, not what it ended up doing. Hence, tests should be written before you code, when all you have are intentions.

This is a paradigm shifter.  There are subtle but important differences between testing what your code does right now and testing against what it was supposed to do .  Tests are questions, the results are the answers.  When you write tests after you code, you tend to test if the code performs as coded.  You’ll find yourself adjusting the test to fit the result, in other words, you’ll be adjusting the question to fit the answer.  TDD forces you to think about all the important questions before writing code.

  • What is the purpose of this function?
  • What are the input parameters?
  • What should it return?
  • How will the rest of my code use this or call this?
  • Are there any edge cases?

Writing Better Code
When you start to write tests for a class, you have to instantiate the class and make calls to the methods and attributes.  This forces you to look at the potential final product from the consumer’s view. By making these calls, you start to think about how you would like other pieces of your own logic to interact with this class.  This is the perfect time to make such decisions because you’re not clouded with existing logic that forces you into making the convenient decision, not always the best decision.

For instance, let’s say you have an object representing a train. You may have a method named *nextStop* that returns the next stop on the train.  You may ask yourself, should this method return

  1. a string value of the stop name
  2. an integer representation (i.e. an id)
  3. a Station object representing the next stop

You now have to think about how you plan on using this method?  Will it be used in a display field somewhere, or will it be used for look-ups and possible calculations?  You may think your code should look something like this to get the string representation of the next stop:

> Train.getStopName(my_train.nextStop())

This would also indicate that you need a method called getStopName and this method may have to be static.  This kind of information is ascertained simply by thinking about the syntax of your text.
Or maybe nextStop returns a Station object, which would allow you to do more analysis of the station.  It may also allow you to chain your requests, like this:

> my_train.nextStop().nextStop()

It all depends how you think the train object should be working.  The key here is that you’re asking all these questions before writing the code, allowing you to think about practical design and usability before getting muddled knee deep in logic.

You Reap What You Sow
When unit tests are written properly and thouroughly, another advantage becomes apparent.  It is far easier for a new developer to understand how to use the code, its intent and expected results by reading the tests than by reading documentation. Tests are actual working code and can serve the purpose of sample code.  By reading through unit tests, the new developer gets acquainted with how the author expects the logic to be called and the expected behavior.

When time comes to upgrade and offer new features and make existing code quicker, unit tests offer peace of mind.  While making changes, you can rest assured that the code still works and that your changes didn’t have unintended consequences in unintended areas of your application.  Since unit tests check the output per given input, you’ll see tests break if any discrepancy occurs after code changes.
You also get better information on where the issue lies.  Without tests, you often find that an error in one part of your code is being caused by an obscure call to another, seemingly unrelated, part of your code library.  With unit tests, the tests are executed and break on error where the unexpected behavior starts.  This lets you focus on fixing the bug rather than spending time tracking it down.

Tips

For all this to work as advertised, there are certain guidelines that should be followed.

  1. Make your tests granular. Your test should test one behavior of one method.
  2. Each method should have a set of tests, each testing different aspects (ie. different input value types).  For instance, if your method takes two integer parameters, you’ll probably want to have a test for positive numbers, negative numbers, zero, null entries, respectively.
  3. When asserting if the result is correct, have very detailed fail messages when it doesn’t work.  Make the method name detailed, even if it’s long.  This will pay dividends when you’re running tests and looking for bugs.
  4. In a given test, test only one method.  Be specific. A test class should be limited to the class or library.  Each class or library should have it’s own test class.  If you start to test other parts of your code, your tests will become less useful when breaks happen because you won’t be able to quickly tell where the error is occuring.

Leave a Reply