The thing I don't get the most is, how can a unit test tell me whether
my calculate price function (which can depend on things like the day
of the week and bank holidays etc, so assume 20-40 lines for that
function) is correct? Would it not be quicker for me to write all the
code and the sit through a debugging session to test every eventually
of the code?
Let's make this the example. If you sit down and write that (let's call it) 30-line method, you're going to try to think of all the possibilities, write them into the method, then debug it, again trying to take all possibilities into consideration. If you've gone as far as checking for days of the week and got to bank holidays, and find a bug, then you need to change the method, and it would be easy to change it in such a way that it now works correctly for bank holidays, but not for weekends - but since you already checked for weekends and it worked, you might forget to re-test. That's just one scenario for bugs to creep into your product with your approach.
Another problem is that it can be easy to add code for conditions that never occur in fact, out of excessive caution. Any code you don't need adds complexity to your project, and complexity makes debugging and other maintenance tasks harder.
How does TDD protect you against these issues? You start by writing the simplest case, and the simplest code that will pass it. Say your default price is $7. I'll write this Java-ish, 'cause it's handy, but the approach works in any language:
public void testDefaultPrice() throws Exception {
assertEquals(7, subject.getPrice());
}
public int getPrice() {
return 7;
}
Simple as can be, right? Incomplete, but correct as far as we've gone.
Now we'll say the weekend price needs to be $9. But before we can get there, we need to know what days constitute weekend.
public void testWeekend() throws Exception {
assertTrue(Schedule.isWeekend(Weekday.Sunday));
}
public boolean isWeekend(Weekday.Day day) {
return true;
}
So far so good - and still very incomplete.
public void testWeekend() throws Exception {
assertTrue(Schedule.isWeekend(Weekday.Sunday));
assertTrue(Schedule.isWeekend(Weekday.Saturday));
assertFalse(Schedule.isWeekend(Weekday.Monday));
}
public boolean isWeekend(Weekday.Day day) {
return day == Weekday.Sunday || day == Weekday.Saturday;
}
Add as many assertions as you need to be confident you have driven the method to correctness.
Now back to our original class:
public void testDefaultPrice() throws Exception {
assertEquals(7, subject.getPrice());
}
public void testWeekendPrice() throws Exception {
subject.setWeekday(Weekday.Sunday);
assertEquals(9, subject.getPrice());
}
public int getPrice() {
if (Schedule.isWeekend(day))
return 9;
return 7;
}
And so it goes. Please notice, also, how we are test-driving the design of our code here, and how much better it is because of it. With your approach, most programmers would have built the weekend-testing code into the body of getPrice()
, but that's the wrong place for it. Plenty of code might want to know whether a day is on the weekend; this way you have it in a single, well-tested place. This promotes reuse and enhances maintainability.