What should you do to make sure new code works properly when you’re refactoring old code? I asked myself this question recently, when I needed to refactor a big bunch of procedural PHP code into a neat, testable, OOP-style interface.
The conclusion I came into is that you should write unit tests – not to test the old code, but as requirements for the new code.
Tests as requirements
When you are coding a new feature, you usually have some simple requirements.
Send an email, put something in the database, and so forth.
When the codebase has grown without much limitation, as was the case for my task, the code becomes a mess of different requirements. For example, there could be a 300 line function which performs 10 different things – Good luck figuring that out!
A solution to this is of course to go through the code and write down all the things the code does, but why not take this to the next level and write the requirements down as tests?
Essentially what I’m suggesting is “reverse TDD” – You already have the code, now you write tests. Except I think that’s just called writing tests, but “reverse TDD” sounds like a proper term, right? ;)
What if the old code is untestable
As I mentioned, the old code could be written in a fashion which is difficult to test.
In this case, you can just ignore writing any content for the test. Simply write test stubs which have a descriptive name (all tests should always have a good name) and make sure they are marked incomplete or fail. You can fill in test-code inside the stubs once you begin writing new code.
Why this is better than just writing down what the code does
Even though this approach requires some more effort than just writing the requirements down to, say, a piece of paper, I think it’s worth to do it this way.
When you have written down the requirements as tests, you can easily see what features you’re still missing, which of them you have already implemented and so on.
When you are done, you already end up with code that has already been tested. Benefits everywhere!
Example
Let’s say you have some code which adds a private message to a user’s inbox. It also sends an email to notify the user of a new private message.
How would you distill these requirements into test cases?
You could, for example, have tests such as these:
- testPrivateMessageAddedToInbox
- testEmailIsSentWhenPrivateMessageIsAdded
Now you have to requirements that are quite clear. Now you can begin implementing the tests, and begin refactoring the code.
When working on the code, you may find that your tests aren’t precise enough. This is okay and quite normal – just add some more tests to make sure all cases are secured.
You may also find that you need split the refactored class into more than one. In this case, you can simply move your relevant test cases into the new class’ tests.
Conclusion
You can make refactoring old code much easier when you have a list of things to do. Unit tests are a quite natural way to approach this: You have a clear list of things, which even show you whether or not it works/is done.
Write a test per each feature the old code does. It doesn’t necessarily have to test the old code, just act as a reminder and a placeholder for test code for the to-be-written replacement code. Later you will thank yourself for doing it.
You may also be interested in:
How to write code that is easy to test