I recently read two blog posts about writing tests for existing (often “legacy”) code. Jacek Laskowski describes (in Polish) the moment when he realized that TDD actually means development driven by tests, starting with tests, and as such cannot be applied to existing codebase.
I can think of at least several cases where you can do TDD on existing, even not-so-pretty legacy code.
When you’re about to add a feature to existing code, start with a test for that single feature. When you see a bug report, write a test for it. Sure, it probably won’t cover much more than this feature or bug, but it can drive your development and guard against regression in future.
Understanding / Documenting Code
When you learn a library, you can try spikes in form of unit tests. It may be much more exhaustive and beneficial in future than a “breakable toy” that you throw away after use. You can use it as documentation or ready-to-use examples in future, and it may even protect you against bugs or incompatibilities introduced in new versions of the library.
You can also try the same trick on legacy code. As Tomek Kaczanowski points out, those tests will often be high-level, integration or end-to-end tests. That’s better than nothing and can be a good starting point for refactoring.
Is that TDD?
One could say that this is not test driven development. I would argue that the whole point of TDD is not a fanatic red-green-blue cycle. It is introducing small, fast, focused (as much as possible…), automated tests that become “live” specification and documentation, and protect you from regressions.
Yes, there is focus shift. There is no “red” phase. Moreover, in a way you write tests after code, even though the benefits left after the process are the same.
I’ve spent a few years on a fairly large project full of legacy code. And I mean legacy, sometimes in the facepalm way.
Whenever I start a piece of work, be it bug or feature, I try to think about tests. If it’s a rusty, legacy area, I may spend a while understanding the codebase. As I do it, I may leave tests for existing code as breadcrumbs. Very often they reveal weaknesses and beg for refactoring. Sometimes I may do the refactoring in place (if it’s very important, or easy), other times leave it for the future.
Sometimes I do know the area that I need to deal with, but it is pretty convoluted. Again, I may spend quite a while trying to write the first test for my new piece of work. But as I design this test, I carefully lay out and understand all collaborators and think about flow. Think of it: designing a test alone can help you understand codebase and the task at hand in much more depth, and feel much more safe about what you’re trying to do.
Once the first test is in, new tests are usually much easier and we’re back in the fancy red-green-blue groove.
There is much more depth to it. TDD is not limited to designing new code in green grass projects. Tests can also help you understand all the dependencies and conditions in existing environment. In other words, think carefully before hacking.
I would say it’s equally, if not even more beneficial, than on the green grass.