Quite recently I attended a training for Agile Developer Skills. It was a great opportunity to revisit and update my understanding of Test-Driven Development (TDD).
Why Test-Driven Development?
The point of TDD is to write code that is modular and testable. Think of a test as the first user of your production code: If even you struggle to write tests for your code, how hard must it be for another developer to use it in the context of a real application?
The TDD Cycle
Test-Driven development is a cycle of three stages
- writing test code
- writing implementation code
Just take a look at the diagram:
The importance of refactoring
My approach to TDD so far had more been Test-first - writing a failing test, then implementing functionality, and by this making the test pass, and repeat. I wasn't aware that refactoring is only meant to take place during the "green" phase, so you are sure that nothing breaks during refactoring, and you stay on the "green path" most of the time.
I was never that strict about it, but it makes sense to have the ability to check whether your refactoring is still a valid implementation. It also leads to working in significantly smaller steps, which is good for your motivation - you hardly ever feel stuck and seem to make constant progress.
Refactoring repeatedly also leads to more concise and clean code - you don't defer cleaning it up to "when you have the time" (which you never will have).
Green bar patterns
Another thing I learned was that there are three well-defined workflows in TDD, which Kent Beck introduced in "test-Driven Development by Example".
The workflows are named "Green Bar Patterns" (the green bar being the indicator that your tests are still passing). These patterns help you to be working on the green path, or to return to it as soon as possible.
This is the approach I used to follow almost exclusively: just solving the problem at hand, no matter how hard it is. But the obvious implementation often is not as obvious as it might seem.
I remember getting stuck a lot when trying to come up with a solution, even for a small problem, and I was subconsciously too proud to take small steps, and overestimated my abilities.
This approach also easily leads to problems when pairing, as the "driver" is hacking away on the red path, assuring that the solution is just around the corner, while the navigator is puzzled and doesn't want to interrupt the flow of the driver.
Sometimes you even have to throw everything away and need to start over, or the solution is only understood by the driver, because the navigator is not emotionally invested in the solution.
The lesson learned is that you should only follow this path if the implementation is absolutely trivial - as the name suggests, obvious. If you find yourself coding an obvious implementation, but fail to get your tests to pass, it's time to switch to one of the following approaches.
Fake it (till you make it)
This approach is forcing you to work in very small increments, until you find a pattern or algorithm that solves your problem. When you start with a failing test, it's fine to (for example) just return a static value at first.
The idea is to get the test to pass as soon as possible. Once it is, you can refine the fake in the refactoring phase. You can always check if you are on the right way, just run your tests, they should always pass.
This approach is great if you already have an idea about a possible implementation, but can't quite see it through. The small increments slowly lead you towards your goal.
In contrast to the "Fake it" approach, triangulation suggests adding more test cases in order to come up with a solution. This is helpful if you realise you are faking it, but NOT getting close to making it, if you have no idea how to implement the solution.
Having another test gives you another perspective and also gives you a security net - your implementation fulfills a constantly growing set of criteria. If you are not sure where to go with your implementation, triangulation is worth a try.
Once you feel more secure about your implementation idea, switch back to "Fake it" or "Obvious implementation" - but remember that your test code is just as valuable as your production code, and refactor your test code as rigorously as your production code.
I don't really think that Test-Driven Development is inherently superior to other forms of programming. I think of it more as a mindset, or a way of life. To me TDD has a spiritual, buddhist feel to it, as you are very much in the moment, taking one step after the other, without hurrying or worrying what is around the corner. I'm very much reminded of a specific koan.
For me Test-Driven Development is about feeling at peace and staying sane while programming, and not so much about the result - but still, I think I deliver better code when employing TDD.