November 22, 2005

...Learn TDD with Codemanship

Write The Code To Pass The Test !!!

One thing that it's always good to remind ourselves of is one of the golden rules of test-driven development - you only write the code needed to pass the test. In practice, this means writing the simplest code possible to execute the test and satisfy the assertions. You MUST write this code, and not the code you think you ought to be writing. If it's the wrong code, then you've written the wrong test.

Take this sample unit test:

------------------------------------------------------

public void testWithdraw() throws Exception {
Account account = new Account();
account.deposit(500.00);
account.withdraw(500.00);
assertEquals(0.00, account.getBalance(), 0.00);
}
-------------------------------------------------------

You might be tempted to write this code to pass the test:

-------------------------------------------------------

public class Account {

private double balance = 0;

public void deposit(double amount){
balance += amount;
}

public void withdraw(double amount){
balance -= amount;
}

public double getBalance(){
return balance;
}
}
---------------------------------------------------------

But that's not what the test tells you to write. The test tells you that you only need to write:


---------------------------------------------------------

public class Account {


public void deposit(double amount){

}

public void withdraw(double amount){

}

public double getBalance(){
return 0;
}
}
-------------------------------------------------------

And that is the code that you MUST write, since that's what test-driven development means. If you don't do the simplest thing to pass the test, you're not really being test-driven. Test-driven means writing the code that the tests tell you to write.

The skill in TDD is building an instinct for writing the tests that will compel you to write the correct code. For example, we could extend our testWithdraw() test like this:

--------------------------------------------------------

public void testWithdraw() throws Exception {
Account account = new Account();
account.deposit(500.00);
account.withdraw(500.00);
assertEquals(0.00, account.getBalance(), 0.00);
account.deposit(250.00);
account.withdraw(175.00);
assertEquals(75.00, account.getBalance(), 0.00);
}
------------------------------------------------------------

Now things are starting to get a little more interesting. The least code we can write now would be:

------------------------------------------------------------

public class Account {

private double balance = 0;

public void deposit(double amount){
balance += amount;
}

public void withdraw(double amount){
balance -= amount;
}

public double getBalance(){
return balance;
}
}
-----------------------------------------------------------

(or is it? - answers on a postcard please).

The value in doing what Kent Beck calls "triangulating" with our tests like this is that we want to ensure the absolute minimum complexity in our code required to pass the test. It also gives us much better logical test coverage and ensures our tests will pick up more subtle bugs if and when they're introduced.

I've lost count of the number of times I've seen tests like this:

------------------------------------------------------------

public void testMyPanel(){
MyPanel panel = new MyPanel();
assertNotNull(panel.getOkButton());
}
-------------------------------------------------------------

What happens if the developer's forgotten to add that button to the panel's components collection or forgot to hook it up to the correct action listener (observer, for non-Java people). The mantra is "test anything that could possible go wrong", and I suspect a lot of developers don't quite appreciate just how disciplined that requires you to be.

You can get an idea of the effectiveness of your unit tests using a simple tool written by Ivan Moore called Jester. What Jester does is inject random changes ("mutations") into the code being tested, and then you can see how many of your tests actually fail. In theory, if your logical test coverage is 100%, changing a line of code should break at least one test.

Yes, I know it all sounds like very hard work, but the resulting software is verging on bullet-proof, and in the medium-to-long term your productivity should actually improve because you spend a lot less time fixing bugs.

If you want to know just how test-driven you really are, here are two simple indicators:

1. Code coverage of tests. If it's less than 100% then that means you wrote more code than the tests required.
2. Jester - if Jester can mutate your code without breaking your tests, then you're probably not being strictly test-driven. The more Jester can change that goes unnoticed by your tests, the less test-driven you must have been.

So how far should one go with TDD? Well, I think you should try to go ALL THE WAY... You want to add a JPanel to a JFrame - you need to write a test for that. You want to make the default balance zero in the constructor, you need a test assertion for that, too. Every bit of your code needs a test before you are allowed to write it, and it must be a test that compels you to write exactly that code because it's the simplest way of passing the test.

And what about acceptance tests? Well, the rule holds for them, too. If the test script doesn't explicity require you to implement a feature (or part of a feature, no matter matter how small) then don't implement it. Do only what you need to pass the acceptance test. If it's not what the customer actually wanted, then you've agreed the wrong test. You're not doing them any favours by reading between the lines and making assumptions about what the tests imply. It opens up a hole in the requirements that you can easily drive a bus through, and effectively teaches the customer to put less thought into defining their requirements. (Basically, it makes you responsible for knowing what they want - which rarely works out for the best...)
Posted 15 years, 2 months ago on November 22, 2005