Taming a Wild, Testless Code Beast
blog.fogcreek.com
blog.fogcreek.com
Gave up after awhile. It's a poor use of time. The app's been maintained for years, its failure modes are well-understood. It's stable. There are other parts of the infrastructure that need attention.
I managed to convince my superiors that now's the time to replace it. Rather than devote countless hours to this time sink, I decided to stop adding to the legacy app, cordoned it off and now treat it like a Superfund site. I replaced my work machine a while back, I keep the old machine around because it's the only thing I can work on it with. I feel like I'm putting an environmental suit and gloves on every time I open it up. All new functionality is built outside the app and interacts with it as if it were a black box.
We now have an agenda and a plan to replace it. And the projects I'm working on now will bring a lot more value and capability. We even got them to expand my department.
What I'm saying is that organizational problems shouldn't be treated like technical problems. A large, untested, legacy application that a lot of business is riding on is an organizational problem and not a technical one. They decided not to devote the resources to properly maintain it, you need to put forth the case that it's a serious problem and that it needs more than your efforts to fix. That's how organizational problems are solved, one person takes the initiative and gets people behind him and makes it happen. Not by being a cowboy.
Trying to take code that was written in one era, and bring it into another era is a huge waste of time and resources. Figure out what you need to really solve the problem and ask for the resources to do so, after you've gotten your ducks in a row and at least made efforts to do it other ways. Your career and your sanity will be better off for it.
Before you even get to step 1, chances are that if you already have a "testless beast", it's not testable. That is, the right abstractions aren't in place to be able to properly mock or fake your dependencies.
To me, this is one of -- if not the -- hardest part(s) of adding tests to an already existing codebase. This is particularly true when dealing with a statically-typed language.
This is why you'll often read how important it is to start an application with testing in mind: as you write your tests, you'll be forced to make your app testable. Otherwise, it's extremely easy to forego some of the necessary abstractions needed for testing -- and conversely, extremely hard to make sure you have all the necessary abstrations in place if you don't include testing from the beginning.
So this article shouldn't be read thinking that it's "that simple" -- the hardest part isn't even mentioned here.
While there is no easy answer, I've found that in these situations, I still try to write a test first. The difference being that I try to write a test that exercises the code at some API level that I don't want to break. These test start off really ugly and require a ton of work, but they ensure that refactoring the lower level code to make it more testable doesn't break things. Eventually, the tests and code at the higher level can be updated safely.
Tests aren't an end; they are a means to assuring proper function, but also towards understanding and writing better software (by forcing you to make code testable, which naturally leads to decoupling, etc).