Most of us know we ought to have decent unit tests for our code. This holds true in just about any project, but possibly most so in agile ones. In agile, you want to deliver working software very frequently, want to practice emergent design, want to take on any challenge the customer throws at you, even if it takes you in a direction very different from the original one. To be able to make such course changes, you need ways to verify that the system and all its parts still work as intended: you want it to be under test. If ever you have been asked to makes change in existing code, you were probably hoping to find such test in place.
Yet, many developers don’t like writing or maintaining such tests. Job’s done, functionality seems to work, other pressing matters want our attention. So at best the unit tests are written in broad strokes afterwards, possibly to meet some code coverage minimum or such. Oh, and aren’t they dreadful? Whatever small change you work on, you’re bound to loads of tests, for no apparent reason and with little clue about what actually broke where. Unfortunately, that’s what happens when tests are added later, for the main goal of “they should be there”. These tests find few bugs, don’t accurately pinpoint problems when they fail, and hinder technical (re)design more than they help it.
Most of us also know we ought to have code that is made up of components with a specific responsibility each and as few interdependencies as possible, of highly cohesive, loosely coupled components. This makes code maintainable and makes it possible to let your technical design evolve. After all, when your code is like a ball of spaghetti in which everything is interwoven with everything else, making even a minor change is a nightmare.
Test Driven Development helps in both areas: it helps you create code that is both well designed and under solid, useful test coverage. Hence its place among the agile engineering practices.
What is Test Driven Development? Test first development is done by following three laws [from The Clean Coder by Robert C. Martin]: 1. You are not allowed to write any production code until you have first written a failing unit test. 2. You are not allowed to write more of a unit test than is sufficient to fail—and not compiling is failing. 3. You are not allowed to write more production code that is sufficient to pass the currently failing unit test.
This locks you into very short cycles in which tests and code grow simultaneously. By consistently writing test code first, you are forced to think about the design of your code. This will almost automatically improve your design, as bad designs make code hard to test. Meanwhile you are describing in detail what the code is supposed to do and you are describing that in the form of executable tests. Doing so you are both documenting your code and providing assurance that all works exactly as described. This assurance gives you freedom to refactor and, on a wider scale, practice emergent design.
Note that law three also means that what isn’t worthy of a test, isn’t worthy of coding either. In other words: don’t add untested error flows and such later on. You are on the other hand expected to actively use the established safety net to refactor both production and test code every time you see an opportunity to improve it.
How do you get started? TDD is most easily applied to parts of the code base with low coupling that address a clear concern: domain objects, business rules and, more in general, when adding new functionality. Additionally when fixing a bug in a project with reasonable test coverage in place, it is usually quite doable to write a failing test first to demonstrate the bug.
Does it work? Does test first really make a difference? Absolutely! Of course you can still make a mess of things, but you will notice that the same amount of effort will bring you to significantly better technical designs and a much more solid test coverage. You will have to stick with it for a while though, to get used to the different way of working and to start noticing the effect on your code base. That takes discipline, because taking a short cut will often seem quicker and in the short run it probably is. Fortunately, in the project I started working this way there was enough latitude to get through this phase and truly see TDD flourish.
And that includes fun: working this way unit tests are no longer brittle, nor tedious to create and maintain; they are an actively used asset that has already paid for itself by the time you first check in code and tests.
And another thing Have I perhaps not told you much you didn’t already know, because you already tried TDD? Cool! But are you per chance struggling with it anyway and wondering where to go now you’ve got the basics down? Then these tips are for you:
Transformations Is TDD sometimes a fluent process for you, while at other times you seem to get stuck in some corner? Perhaps this blog post can help you out. In it, Robert Martin wonders if next to the catalog of refactorings, simple operations to change the structure of your code without changing its behavior, a catalog of transformations could be made as well. Such a transformation is a simple operation to change the behavior of your code one small bit. This is to be an ordered catalog, from which you’d always try the simplest transformation first. His hypothesis is that if you write your tests in an order that prefers the simplest transformations, you will keep that nice rhythm going and avoid TDD-ing yourself into an impasse.
Working with legacy code Starting green field would be very nice, of course, but often the reality is that we’re working on existing code, code with no or few useful tests in place, code we can’t just throw away to start fresh. Where do you go from there? Michael Feathers has written a book dedicated to the topic: Working Effectively with Legacy Code. It is dedicated entirely to ways to isolate (new) parts of the code as safely as possible, so work on these parts can be done with TDD, even though the system as a whole is initially far from under test.
Growing object oriented software The unit tests guide you through building the specific component you’re working on. But, especially in a larger system, with what component will you start? What will guide you towards a fully working system, made up of those nicely tested components? Steve Freeman and Nat Pryce state that your acceptance tests can provide this guidance. They practice TDD from the outside of the system inwards, a way of working they have detailed in their book Growing Object-Oriented Software, Guided by Tests.
Specification by example But wait a minute, those acceptance tests, they capture the requirements of the system, no? So, can’t they just be the requirements? Can’t we make these tests readable by business people, or rather, make the examples and scenarios in their requirements themselves executable by our test system? Absolutely! I believe executable specifications are a logical next step that further helps the agile development team (business analysts, developers, testers) work effectively as one team. Gojko Adzic filled an inspiring book from the war stories of 50 projects practicing this way of working: Specification By Example.