The conventional wisdom is that you should have good test coverage before refactoring your code. Making mistakes during refactoring will introduce defects in the software if they are not caught during testing. Such regression defects can annoy users, break the software and even lead to customers abandoning the product.
In practice, however, there is a chance that you will encounter situations where you need to work in untested or insufficiently tested legacy code. Often this is precisely the kind of code that would benefit from refactoring to make it easier to understand and extend. But to test it, we need to make it testable, which involves refactoring.
How do we resolve this problem? If we take a step back, the underlying question that automated testing helps us answer is a question of confidence and risk:
How confident am I that my refactoring has not changed the program behavior?
Tests help us answer the question, but there is a chance that refactoring introduces bugs when there are gaps in test coverage. Therefore the answer to the question is a matter of degree, even with tests.
Another approach is taking a sequence of small steps. If we are confident that each step is correct, the final result is correct since no step changes the program behavior. Ideally, taking small, low-risk steps is combined with excellent test coverage, leading to high confidence.
But for single functions, classes or modules, it can be possible to chain together sequences of automated refactorings to achieve a more extensive refactoring. However, most refactoring tools ensure that the refactoring mechanics are correctly executed, but they do not tell you about the impact on your code.
Here is an example of how P42 indicates the safety of small refactoring steps: