See the book Working Effectively with Legacy Code by Michael Feathers.
In summary, it's a lot of work to refactor existing code into testable and tested code; Sometimes it's too much work to be practical. It depends on how large the codebase is, and how much the various classes and functions depend upon each other.
Refactoring without tests will introduce changes in behaviour (i.e. bugs). And purists will say it's not really refactoring because of the lack of tests to check that the behaviour doesn't change.
Rather than adding test across the board to your whole application at once, add tests when you work in an area of code. Most likely you'll have to return to these "hotspots" again.
Add tests from the bottom up: test little, independent classes and functions for correctness.
Add tests from the top down: Test whole subsystems as black boxes to see if their behaviour changes with changes in code. And so you can step through them to find out what's going on. This approach will probably get you the most benefit.
Don't be too concerned at first with what the "correct" behaviour is while you are adding tests, look to detect and avoid changes in behaviour. Large, untested systems often have internal behaviours that may seem incorrect, but that other parts of the system depend on.
Think about isolating dependencies such as database, filesystem, network, so that they can be swapped out for mock data providers during testing.
If the program doesn't have internal interfaces, lines which define the boundary between one subsystem/layer and another, then you may have to try to introduce these, and test at them.
Also, automatic mocking frameworks like Rhinomocks or Moq might help mock existing classes here. I haven't really found the need for them in code designed for testability.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…