Monday, November 29, 2010

Making TDD Work

The key to making TDD work is to follow three principles:
1. Write SOLID style, clean code
2. Focus on behavior
3. Always start with the tests

Learning TDD (Test Driven Development) has been a surprisingly difficult process for me over the last few years.  Once I figured out how to think like a TDDer I was pretty well on my way, but it didn't really end there.  The main issue is that bad tests can be worse than no tests at all.  The hard part is figuring out what makes a test bad and what makes a test good.  This post is my attempt to convey what I currently think are some good guidelines for writing good tests.

SOLID and Clean
All the code you write has to be good clean code following the SOLID principles.  Whether the code is a test or "real" code, it has to be good.  If the code isn't clean, well designed, and easy to understand you don't stand a chance.

Some of the things SOLID and Clean Code encourage seem counter intuitive at first.  For example, the concept of creating small single purpose methods, especially when they aren't intended to be reused, seems like it might make it harder to find things, name things, and understand things.  Of course, the truth is the exact opposite.  Creating small, single purpose, well named methods dramatically simplifies your code.  The same is true for creating small, well defined classes.  If you are at all like me you'll have to see it in action to really believe it.  Even today there are times when I have the mental debate of whether to apply a method refactoring, and every time I do it, the code always ends up better.

Following these principles is absolutely essential to making TDD work.  It is impossible to test big ball of mud code, it's difficult to test code that isn't single purposed and well defined, and it's enormously difficult to understand tests written against a code base that isn't SOLID and Clean.  The more understandable the code under test is, the more understandable the tests will be.  In fact, I firmly believe that if you're doing it right, the code under test should end up being so simple you will find yourself questioning if the tests are worth having at all!

Behavior
The tests should focus on the behavior of the system (Behavior Driven Development or BDD).  In fact, I would go so far as to say that if you're thinking about code coverage when you're writing tests, you're doing it wrong.  Thinking about code coverage leads to tests with names that can't be understood without an intimate knowledge of the implementation and which are a nightmare to maintain.  Code coverage can be a useful metric, but it shouldn't guide your tests.

If you instead focus on the behavior of the object you are testing, you should still discover that you approach 100% code coverage, but you will do so in a way that results in meaningful, descriptive, and understandable tests.  This is critically important, because tests are not something you write once and never deal with again! Naming and organization is crucial here.  Typically I follow the When, With, Should naming pattern in my unit tests.  For example, I may have tests like:
[Test]
public void WhenTransferingMoneyBetweenAccounts()
{
  var srcAccount = WithAccountHavingBalance( 100 );
  var destAccount = WithAccountHavingBalance( 100 );

  accountTransferService.Transfer( 50, srcAccount, destAccount );

  Assert.AreEqual( 50, srcAccount.Balance, "Source account should have balance of 50" );
  Assert.AreEqual( 150, destAccount.Balance, "Destination account should have balance of 150" );
}

[Test]
public void WhenTransferingAmountLargerThanSourceBalance() {...}
Each test represents a certain scenario.  The first block of code sets up the scenario.  The second block performs the action we're testing.  The third block verifies everything worked as expected.  This style, inspired in part by mspec and rspec, makes each test's responsibility obvious and helps describe all the scenarios and their expected behavior.

You know you're done testing when you've covered all the scenarios.  And if you find a bug, it should be fairly straight forward to decide what scenario or test is missing.

Tests First
Finally, you should always start with the tests.  When you're writing something new, start with the tests.  When you're fixing a bug, start with the tests.  When you're adding new behavior, start with the tests.  When you're doing a code review, start with the tests.  When you're trying to understand how some code works or what its for, start with the tests.

There are times when you wont be able to fully implement the tests first.  For example, if you're writing code that uploads a file in ASP.NET MVC and you don't know how MVC provides the file data, you wont know what to mock or what to expect until you dive into the code.  That's totally OK.  But you should still frame out the tests first, then come back and implement them (just make sure that the tests fail when expected).

Writing the tests first forces you to think through the behavior you're attempting to build and often helps you see flaws in your object design and algorithms.  Writing the tests first will also encourage you to write single purpose, clean, and well defined code.  Helping you arrive at that point where the code under test is so simple the tests seem almost pointless.  It's possible to end up with code that is simple without tests, but it's much harder.

When modifying existing code, starting with the tests will help you notice when the design has changed enough to warrant refactoring the code and redefining the roles of your objects.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.