One of the great strengths of test-driven development is that systems that are built one tiny test at a time tend to be… well, better. Fewer bugs. Cleaner architecture. Better separation of concerns. The characteristics that make code hard to modify are the same characteristics that make it hard to test, so by incorporating testing into your development cycle, you find – and fix – these pain points early, whilst development is cheap, instead of discovering them three months after you’ve shipped and spending the next two years death-marching your way to version 1.1.
However, there’s a flip-side to this. Not a disadvantage of TDD per se, but something that I think is an unavoidable side-effect of placing so much emphasis on TDD as applied to green-field projects. The “test-driven” part and the “development” part are so tightly coupled that it's easy to assume that automated testing was only applicable to new systems.
I can’t be the only one using Moq and NUnit on new projects, whilst the rest of the time grimly hacking away on legacy code, dancing the Alt-Tab-F5 fandango and spending hours manually testing new features before they go live. And I can’t be the only one who thinks this is just not right – after all, the big legacy apps are the ones with the thousands of paying customers; surely if we’re running automated tests on anything, it should be those?
Last week, two things happened. One was a happy afternoon spent setting up TeamCity to build and man age most of our web projects. The other was a botched deployment of an update to a legacy site – the new code worked fine, but a config change deployed as part of the update actually broke half-a-dozen other web apps running on the same host. Broke, as in they disappeared and were replaced by a Yellow Screen of Death, because the root web.config was loading an HttpModule from a new assembly, and the other web apps were picking up the root’s config settings but didn’t have the necessary DLL. Easily fixed, but rather embarrassing.
If It Runs, You Can Test It
This was a stupid mistake on my part, easily avoided, and it suddenly occurred to me, screamingly easy to detect. We may not have any controller methods or IoC containers to enable granular unit tests, but we can certainly make sure that the site is actually up and responding to HTTP requests.
One of the team put together a very quick NUnit project, which just sent an HTTP GET to the default page of each web app, and asserted that it returned a 200 OK and some valid-looking HTML. Suddenly, after five years of tedious and error-prone manual testing, we had green lights that meant our websites were OK. It took another ten minutes or so to add the new tests to TeamCity, and voila – suddenly we’ve got legacy code being automatically pushed to the test server, and then a way of firing HTTP requests at the server and making sure something comes back.
You can do this. You can do this right now. TeamCity is free, Subversion is free, NUnit is free, and it doesn’t matter what your web apps are running. Because the ‘API’ we’re testing against is plain simple HTTP request/response, you can test ASP, ASP.NET, PHP, ColdFusion, Java – even static HTML.
What’s beautiful is that, once the test project’s part of your continuous-integration setup, it becomes really easy to add new tests… and that’s where things start getting interesting. Retro-fitting unit tests to a legacy app is hard, but when you need to modify a piece of the legacy app anyway, to fix a bug or add a feature, it’s not that hard to put together a couple of tests for your new code at the same time. Test-first, or code-first – doesn’t matter; just make sure they make it into the test suite. If you’re coupled to legacy data models and payment services and ASP session variables, you’re probably going to struggle to set up the required preconditions. But, most of the time, you’ll find something you can test automatically, which means it’s one less feature you need to worry about every time you make a change or deploy a build.
We now have 19 tests covering over 50,000 lines of code. Yeah, I know - that’s not a lot. But it’s a start, and the lines that are getting hit are absolutely critical. They’re the lines that initialize the application, verify the database is accessible, make sure the server’s configuration is sane, and make sure our homepage is returning something that starts with <html> and has the word “Welcome” in it – because I figure if we’re going to start somewhere, it might as well be at the beginning.