Using Test-Driven Development with existing projects

Learn how to know when it's time to refactor in Test-Driven Development, and how TDD can work well even when maintaining poorly designed software.

One of the steps in the cycle of Test-Driven Development (TDD) is to make the smallest code change that will pass the new test you've just added. Emil Gustafsson points out that perhaps the simplest change is better than the smallest change.

To anyone who has maintained non-trivial code that's been around for more than a couple of years, this step may still seem counter-intuitive. We've all seen plenty of counterexamples where somebody added the minimum code to "make it do X," without considering how that change would impact other features or the overall design of the system. As all of these small, unrelated changes accumulate in a piece of software, it begins to resemble a skeleton that compensates for deformities by creating additional, even more grotesque deformities.

The key to avoiding this trouble is in the next step of the TDD cycle: refactoring. Once you've passed the test, then you figure out how it's supposed to fit into the design and gradually migrate it to that point, all the while running the tests to make sure you don't break anything. It's important to continue to take "baby steps" in this process. Sometimes, after making a trivial change to pass the test, the refactoring feels like starting over with "Okay, now how do we really do it?" There's a chance here to fall into the same trap that making the simplest possible change intends to avoid: introducing too much complexity at once. To help eliminate that disconnect, I like to make that simplest possible change elegant in its own way -- the way I would write it if I had no other requirements. Thus it acts as a first, small step into the refactoring process, rather than being the stupidest way to code it. Then I proceed to deal with all the other test failures that my change has provoked. After all, we do have tests for everything, right?

I find that TDD works better if you've done it throughout the life of a project. You have the tests for all expected functionality at your fingertips. Even if they take a while to run, and even if your changes mean modifying a whole bunch of tests, the confidence you have in your modifications means that you can move ahead more quickly overall and introduce fewer bugs in the process.

It's another story when most of the project's prior development did not employ TDD. Even if it has a large test suite, you don't have any guarantee that it covers all the modifications that developers made in the past. You have to read the existing code to understand not only how it does what it does, but why. Even if the design is brilliant, all of the whys may not be obvious. Even when it's your own code.

More often, though, a project that has not previously used TDD (or something similar) will bear an overly complex design. Starting with the big picture, building out all the theoretical abstractions, and finally creating the instances you need means that you end up creating a lot of abstractions that you don't need (YAGNI). Besides consuming the initial development time, that kind of feature can make the code more difficult to maintain by providing one more thing to break -- or at least to consume the mental cycles of the maintainer who is trying not to break it.

You can still use TDD to your advantage on projects that haven't used it before. You just need to draw a clear line between what you know and what you don't know. You'll probably need to research the existing requirements of the system, and you may need to write some tests for those features. Refactoring may involve breaking down a monolithic design. Schedule more time for QA and acceptance testing. Despite all these additional time requirements, employing TDD for your new changes will still result in a higher quality:time ratio, and will begin to build a stronger foundation for future modifications.

Keep your engineering skills up to date by signing up for TechRepublic's free Software Engineer newsletter, delivered each Tuesday.


Chip Camden has been programming since 1978, and he's still not done. An independent consultant since 1991, Chip specializes in software development tools, languages, and migration to new technology. Besides writing for TechRepublic's IT Consultant b...


My observation is that if managers push their people to have fast ticket clearance rates that this will generate flawed unreliable software as a consequence. Even if the programmers concerned are really smart! The manager creates a system within which failures occur, although the subsequent maintenance revenue will increase, and they are able to show the customer they have a systematic approach. Hmmm.... I have yet to come across TDD in the wild, for the reasons Tony mentions. In the stuff I have refactored: I began by sorting out the naming issues. I then address the structural issues. In the stuff I support: I have been able to split the code into chunks that works out what is required to be done, then gathers the facts of the situation and gets any additional data required and then goes ahead and does what is required. I even write less than optimal code if it improves clarity (Gasp). Each step of the process generates contextually identifiable logging entries. (Interactive debugging is not available with the tools I use. I almost think I am better off without it? I love reading my logs.) Everytime I have to follow up on something or do a modification, I create test cases, preferably harvesting them off the production system. (This works for me but may not for you. My presentment work occurs after the transactions are finalised.) It does not take long before a decent regression suite is built up, but in my case the tests are not automated (I wish they could be.) So I exercise judgment about when to do a full suite of regression tests. I have segregated the tests by each aspect of the program to make this easier to achieve. I just run the aspect impacted by the change and hope there are no side-effects. So to summarise: I ensure my work produces software that is understandable. Getting there requires good practices. At each step of the way I have sufficient testing to show I have not stuffed up. In those increasingly rare situations where programs go rogue, I am in a stronger and stronger position to sort them out through good logs and easy to read code. Tony your experiences reflect some of my own. Mercifully I have often been in a position to do something about them, although I have had to run the risk of the need to move on. :-) Agreed, none of this is rocket science. "Write as you would be written unto" Kingsley - [i] with mods. [/i] Phil

Tony Hopkinson
Tony Hopkinson

First of all TDD is a significant change in approach, there will be more than a few get it out of the door types saying things like "Well if we get it right we won't have to test it", or "Yes, that's a great idea we'll do it next version", some of them won't be in management either.... The initial time investment in gearing up to do TDD is significant, and even if you've just done it at school, it's so foreign to the way most of us have worked traditionally (write bugs then, find them), there's a pretty steep comprehension curve. Time to first deliverable vesus lets all code like crazy, so it looks like we've done something will be noticeable. Shifting to TDD on an existing code base? That's all tied up behind shifting to unit testing on an existing code base. That's a a well known issue with some ameliorative techniques that have been round for ages, and basically involves at least some refactoring of the code in order to test it it. Thing you've got to remeber is none of this stuff is particularly new. It's not bleeding edge technology, doesn't require the worlds top coder ever. The technical and business arguments for any piece of software with a significant longevity are inarguable. Yet most of us have never done it, less of us do it all the time, and noine of us could expect a legacy piece of software to be in a state for TDD. There's a reason for that, business does not believe in it. Until we address that, we are in the vernacular buggered, constantly and with great vigour.... Smile...


I started out in db development, but now primaily work with data anlysis. In both situations, I've found that an iterative approach pays off. Even those "wrong roads" that I went down while initially trying to put together a complex report or database may prove helpful at the end. At the beginning of each project, I create an archive file, and I save every useful "bad" version into it. I've often found that even if a particular formula doesn't work for the given application, it may be helpful later.

Editor's Picks