I have worked on a very large code base which initially had no unit tests. By following a few practices we now (after several years) have most of the code base covered by tests.
All new code must have unit tests.
All changed code must have unit tests added to it.
The way that we safely added tests to old code without breaking it is primarily to use the following basic approach:
Choose a small section of code that you need to change the functionality of.
- Try to create system level integration tests to surround the code. Because of the combinatorial complexity of testing at this level, these tests will only form a "smoke" test to pick up major mistakes.
Introduce the interfaces you need in order to be able to test the code you are changing. Use Refactoring techniques consisting of sequences of very small changes which you have high confidence are correct. Try to use tool support where possible. You might do this by, for example, moving/extracting the method you are changing onto its own object. Check in your changes regularly so you can revert. Regularly peer-review how you made the changes by going through the revision control history.
Try to make the minimum about of changes that are required in order to break the dependancies that are preventing you from adding tests.
- Write tests to as far as possible cover the functionality of the code that you are going to change. Check in regularly and peer review all changes.
- Write tests for the new functionality/functionality change.
- Implement the functionality (this is your normal TDD cycle)
- Make sure to refactor the areas you have covered by the tests (red-green-refactor).
We found that the more we did this, the easier it got. As every time you go back to the code base, it is a little bit better.
We have seen a massive drop in the number of bugs getting through to our QA testers. With functionality regressions being now almost unheard of, so I think it was worth the effort for us.