|In this series we're taking a look at how to refactor un-refactorable legacy code.
The current code version we’re talking about is tag 1.1.
Last time, we have surrounded our code with a boundary, doing some early refactoring. We now know the entry points, and the exits are covered by interfaces, leaving code we don’t want to deal with outside our system. We can basically start writing tests, but which ones? How do we know can cover every case?
The answer is we don’t and can’t know. But we do have clues in the code itself. In our case, it’s the conditions in the code. In other cases in our legacy code, it is about identifying the values that can affect the different flows in the code.
In our example, the options are combinations of sauces, pasta types and places. We’ll take a look at those in a moment, but we need to remember to also look beyond the code.
There’s another aspect for our cases that is not apparent in the code. What if the cook method is called more than once? From reading our example, you can see that the IngredientsList is created with each call. However, what if it was created in the constructor of PastaMaker class? Keeping state between method calls can have an affect that may not be visible directly from the code.
In our refactoring example, we won’t go there. For us, each flow inside the cook method is isolated from other flows, and therefore we can cover those flows by different tests, exercising the method once. In more complex cases in legacy code, where state of one method call affects consequent calls, we might consider better coverage, by calling our code once, twice and maybe more and in different constellations.
Gotta catch them all
If we want to cover all possible combinations in our code, we’re going to need 4 sauces x 2 pasta x 4 places = 32 tests. For one lousy method. Imagine how many combinations we have in the legacy code counterpart. If want to be thorough, we need to cover them all.
If we’re feeling a bit gutsy, we can start eliminating cases. For example, if we look at the places, we can see that they don’t affect the conditions, only the results. That means, in the current code, we can consider the cases for the same pasta and sauce, but with different places as equivalent. Meaning, we can reduce each place quartet of tests to a single test.
I am feeling a little gutsy, so I’ll do that. Now the only question remains: How do we test?
We’ve already talked about characterization tests. To implement them, we’ll need to write a test for each test case that we’ve identified, run and see what we get, and specify the actual result as the expected one.
And what is it that “we get”? In our case, what ever comes out of the system. Meaning, every call through the IDispenser interface. We need to log all the calls, in the order they come out, with all the parameters.
The option I’ll take here is with Approval Tests. I’ll talk about it shortly, and go into the tests next time.