A quick refresher - TDD is simple, but not easy.
- Write a failing test
- Write the minimum code to make test pass
- Refactor
You have to think at each step. This is often overlooked, and TDD portrayed as series of mindless steps (if that were true, developers probably wouldn't get paid so well!).
Let's take the Bowling Kata as an example of how easy it is to fall into mindless steps. What's the right test to do first?
[Test]
public void PinExists() {
Assert.That(new Pin(), IsStanding, Is.True);
}
We're bound to need a Pin class right? And we should definitely check whether it's standing up or falling down. We continue in this vein, and create a Pin that can be knocked down and stood up. Everything proceeds swimmingly. 15 - 20 minutes have elapsed and we have a unit tested equivalent of bool that's no use to anyone.
I've seen similar anti-patterns in the Game of Life kata. We write some tests for a cell, and the rules (three live neighbours means alive and so on). We use this to drive a Cell class and we add methods to change the state depending on the number of neighbours. Some time passes, and then we realize we actually want a grid of these objects, and they change state based on their neighbours state and we're in a bit of a pickle. Our first guess at an implementation has given us a big problem.
If we somehow manage to solve the problem from this state, we end up with a load of tests that are coupled to the implementation. Worse, because we've ended up creating lots of classes, we start prematurely applying SOLID, breaking down things into even more waffly collections of useless objects with no coherent basis. Unsurprisingly, it's difficult to see the value in test-driven development when practiced like this.
So what's the common problem in both these cases?
Uncle Bob has described this behaviour Slide 9 of the Bowling Kata PPT describes a similar problem, but attributes it to over-design and suggests TDD as the solution. I agree, but I think some people pervert TDD to mean test-driven development of my supposed solution, rather than TDD of the problem itself.
The common problem is simple. Not starting with the end in mind!
If we'd have started the Bowling Kata from the outside-in, our first test might have simply bowled 10 gutter balls and verified we return a zero. We could already ship this to (really) terrible bowlers and it'd work!
Maybe next we could ensure that if we didn't bowl any spares/strikes it'd sum the scores up. Again, now we can ship this to a wider audience. Next up, let's solve spares, then strikes and at each stage we can ship!
Each time around the TDD loop we should have solved more of the problem and be closer to fully solving it. TDD should be continuous delivery, if the first test isn't solving the problem for a simple case it's probably not the right test.
Similarly for the Game of Life, instead of starting from a supposed solution of a cell class, what happens if your first test is just evolving a grid full of dead cells? What happens if we add the rules one at a time? You can ship every test once you've added the boilerplate of the "null" case.
TDD isn't about testing your possible implementation on the way to solving the problem, it's about writing relevant tests first and driving the implementation from that. Start from the problem!
TDD done right is vicious - it's a series of surgical strikes (tests) aimed at getting you to solve the problem with the minimum amount of code possible.