I was recently having a discussion with some coworkers about test-driven development. There was some discussion about the relative value and cost, and not surprisingly some dramatically different opinions on the subject.
It got me thinking about my own habits. I like test-driven development, but I’m not a purist. I almost always write my code with testing in mind, but I don’t always write tests first. Most of the time, but not always.
When I do write the tests first, I find that the process works just like the books say it should. That is, I write the tests and in doing so define the interface that I want to use. By the time I have what I believe to be a complete, failing test, I have identified the behavior I want along with a number of edge cases. Once I start the implementation, coding is fairly straightforward and is usually done more quickly than I would have expected.
When I’m done with the implementation, I have a high degree of confidence that my code will work and that the interface is usable. After all, I’ve already used the interface in the test. What’s more—the test itself provides a useful, working example for other team members that may need to work with the code.
If I later find that my implementation was naïve, I already have tests that will allow me to refactor to a different implementation without inadvertently changing the behavior that existing code might be relying on. Whether I need to improve performance, handle a newly-identified use case, or add some different options, I can do so with a safety net of tests that will quickly show the effects of my changes.
On the other hand, I spend less time over-designing and over-engineering my implementation. Since my primary goal is just to get tests working (not my sole goal, remember I’m not a purist), I tend not to spend time developing for situations that may never arise (“the art of maximizing the amount of work not done“). Again, I know that my tests are there to help me or whoever comes after me, to add any features if and when they are needed.
So if I like it so much, why don’t I always write the tests first?
Sometimes I’m just writing code that is difficult to test, for example client-side code. It is notoriously hard to write automated test for user interfaces. I usually opt not to write tests and instead test manually and rely on the QA folks to make sure everything is working properly. My reasoning is that the QA folks are going to have to run through that interface anyway, and there should be very little business logic involved if I’ve laid out my design properly.
It isn’t always an explicit decision not to write my tests first, though. Once in a while I will get overanxious and I’ll jump right into coding an implementation because the solution seems straightforward. If I’m right, then the problem was probably fairly small or at least well-defined, and not writing the tests first doesn’t burn me. If I’m wrong, I usually realize that I underestimated the complexity of the problem. Hopefully, I realize this early, push whatever code I’ve written for the implementation to the side so I can go back and write the tests before continuing. Other times I just get wrapped around the axle trying to solve the problem and by the end I’ve reminded myself why I try to stay diligent and write the tests first.
Once in a while, I’ll skip straight to an implementation when the problem is complex, only because I can’t think of how the tests and the code should work. Staring at the screen hoping for inspiration on how to write the tests is not productive. So I just start coding, hoping that as I meander my way towards a solution, the design will become clear. At that point, I try to push my implementation to the side and go back and write my tests now that I understand the problem.
In the end, writing the tests first is just a tool that I use to develop code quicker. I try to follow the practice only because it helps me to deliver code quicker than I otherwise would—with documented behavior (in the form of tests) and with high confidence that I’ll be able to safely change the implementation later if the need arises. I don’t blindly follow test first development because it is part of Agile processes. I practice test first development only when makes sense to do it based on my experience, that experience has taught me that in the right context I develop code more efficiently doing it that way.