Legacy Code Testing Interview Questions
Is there no way legacy code can own you?
Question 1
Can you describe the sprout method in regard to refactoring legacy code?
In short, the method is about extracting a piece of code into a separate class or public method. It allows you to work on a change in isolation without the fear of breaking other code. It also makes introducing code to test harness easy thanks to extraction. It is a flag example which follows the Single Responsibility Rule.
  • Adding a new feature
  • Fixing a bug
  • Improving the design
  • Optimizing resource usage

The first two are relatively straightforward when it comes to introducing automated tests. At least compared to the other two. Each bug can be treated as a feature on top of that and vice-versa.

What developers generally detest is improving the design as it is relatively easy to loose the existing behaviour and introduce a bunch of bugs. You also need to bring your A game once this task has been assigned to you. It almost always a challenge.
I am sure you have heard that you should cover the public method that needs a change with a full set of unit tests before even touching it.
This is sort of a TDD for the legacy code testing.

That might be true if the method is relatively small. That happens very rarely though in a legacy codebase.

The safest way, where you do not need to waste a week covering all the possible logical paths with tests, is to take advantage of the Sprout Method / Class.

You basically extract the smallest part of the public method possible into a separate class.

You cover that with tests, and then you could start a few TDD cycles to get the actual change.
The main idea behind this rule is to make the proximity of you change better.
After writing tests, try to add missing comments on interfaces, raname variables etc.
Also as we are dealing with legacy stuff, try add some additional tests to the proximity of your change.
Make this a habit and a change in software quality will come sooner than you think.
A mixture of refactoring and automated testing is in general the only way.
The way you refactor is up to you. The key here is to extract small enough chunks of code so that they can be easily tested. Spout method is an excellent choice here.
Then the change can be applied on one or many of the chunks and cemented with even more tests.

When it comes to refactoring there is a choice between some automated refactoring tool offered by most modern IDE's or manual refactoring. The second one is of course more challenging but the lessons we take from that approach are priceless.
It is used when:
  • The method is too complex for it to be unit tested efficiently.
  • The change to be applied goes at the beginning or the end of a certain feature.

Having that said, what we do next is to create a new method that calls internally the original method.
Then using TDD we apply the change in the wrap method without touching the original one.
Here are a few points to follow in regard to framework methods handling:
  • The code that directly interacts with 3rd party libraries should be isolated and treated as the only point of entry for the rest of the code.
  • It should be thoroughly tested. We should not mind going for 100% coverage here. It will most likely be the code of our app and it should be flawless. That amount of coverage gives us also freedom to refactor the code as we please to maximize its readability and maintainability.
  • We should try to add some load testing on top of plain unit tests. How does the system behave when hundreds of threads call a single 3rd party library?
Characterization tests are done on a piece of code which behaviour we want to preserve.
The only reason for the test is to help us figure out how the code works.
An implicit benefit, once we find a way the test to pass is the increase of coverage.
A seam is a place in your code (a hook) thanks to which you can change the behaviour without the need to edit to code in place.
The behaviour can be altered in a way that there is no need to break existing dependencies. This is very important in regard to legacy testing.
  • Using a mocking framework such as Mockito.
  • Breaking the code into smaller, specialized classes.
  • Using seams / hooks where it is highly likely that the behaviour may change or be dependent on the environment.