Safely Refactoring Legacy Code with Automatically Generated Unit Tests
Legacy code is always a minefield, whether you’re trying to understand, modify, refactor or migrate away from it. In this article, you’ll learn to use the automated unit test writing tool Diffblue Cover to understand opaque code and to create a set of tests that will keep you safe when working with it.
The Situation
The Tennis Refactoring Kata imagines a situation where you must work on a piece of code that is completely unknown to you and undocumented. Perhaps your colleague is away, or has long left the company, but your group still has a deadline to meet.
While this does happen in the modern world of software development, it is also analogous to another common situation-having to understand, fix or refactor some legacy spaghetti code that no one in the company understands anymore.
Writing Tests Automatically
I have converted the original kata code into a form that focuses precisely on the scoring logic that you want to test (and let Intellij Idea tidy the formatting). The `announceScore` method takes the number of ‘games’ that each player has won and converts this into ‘tennis’ terms — `Love-Fifteen`, `Advantage Player1`, etc…
Now, pretend that you have no idea how tennis scoring works. `announceScore` would be somewhat impenetrable, given all of the nested if/else statements, the different code formatting used (how many people worked on this code again?) and the weird loop at the end. Legacy code often has methods 20 times longer than this, with even more cyclomatic complexity! You’re rushing to meet this deadline and get the updates shipped-there’s no way you have time to prod and poke at this code until you think you understand it. Even if you do investigate it extensively, would you ever be comfortable that you’ve caught all of the edge cases put in there by all the developers who came before you?
Let’s see how Diffblue Cover can get you out of this predicament and still meet your sprint goals.
Setup
There are a couple of requirements you need to meet to get Diffblue Cover working.
1. The code must compile — which it does
2. Diffblue Cover must find some required dependencies on the classpath — here this is just JUnit, but this can be for example Spring (Boot) Test or for Spring (Boot) applications
Add JUnit 5.6.2 to the pom.xml or build.gradle to meet the latter requirement.
Writing
Diffblue Cover can write tests at a per method level, per class level or for many classes at once. Here there is only one method you need to analyse, so right click on `announceScore` and choose `Write Tests`.
Diffblue Cover will start an incremental build in Intellij Idea, and will keep you updated with the progress it is making on writing tests in both the Background Tasks window and the Event Log.
In well under a minute, it’s done!
Tests
Twelve new tests have appeared in the newly created `test/java/…` directory in the `TennisScoreTest` class — the naming convention for generated tests is also configurable, by the way.
Since Diffblue Cover has observed that these all affect the same method, all 12 test assertions have been combined into a single test-again, this is user configurable, so it is possible to generate 12 different test methods instead.
I think the output here is extremely interesting. Remembering again how tennis is scored an obvious bug jumps out at you straight away-the method accepts inputs that represent fewer than zero games won, which obviously makes no sense! In some cases it even produces clearly incorrect output without a score for one of the players (`-Forty` in the final line).
This surprise aside (how has this bug gone unnoticed for so long?!), observe that Diffblue Cover has actually covered every possible case for tennis scores:
You might think “Ah-ha, but it hasn’t tested every possible combination: Love-Fifteen, Fifteen-Love, Love-Thirty, Thirty-Love, Fifteen-Thirty, etc…” Actually Diffblue Cover is smart enough to know that when the score is not an ‘All’, ‘Win’ or ‘Advantage’ case, the code is the same and behaves the same for both players’ scores — so it only tests all scores for one player. This prevents you from having redundant tests, which slow you down when you make future changes to the code and subsequently have to keep going back and updating test after test to get through CI.
Coverage
We can run this test and check the coverage in Intellij Idea. Confirming what I wrote above, the coverage of the scoring method is 100%! Now you can go ahead and refactor this horrible method to something nicer, without the fear that something will break.
Exception Handling
So far all of the inputs have been ‘happy cases’, in the sense that the method does not throw any exceptions. Let’s first refactor our `announceScore` method not to allow negative inputs. Change the method to throw an `IllegalArgumentException` in this case.
Your newly generated test will now fail, when it hits one of the assertions that use a negative input. Before generating updated tests you can delete either all the assertions in the test method, the entire test method or the entire test class-Diffblue Cover will either insert the new assertions into the empty method, create a new method or recreate the class file as required. Go ahead and delete all the assertions and just leave an empty test method shell. Right click on the `TennisScore` class and choose `Write Tests` — this time you can watch the assertions appear!
Diffblue Cover has recognized that the method now throws exceptions for certain inputs and writes `assertThrows` statements to test that behavior. It understands the sad path as well as the happy path!
State & Object Creation
One other refactoring you might consider applying is wrapping the counts of games won in an object and making `announceScore` take that object as an input. This attaches more meaning the values we pass into the method, and makes the code better self-documenting. In the refactored code, note that I’ve used an inner class to stay in one self-contained file — Diffblue Cover would have no problem if `GamesWon` were in its own file ( try it yourself)! Clear out the old tests again and Write Tests on the `announceScore` method.
You’ll see that Diffblue Cover was able to instantiate the required `GamesWon` instances to pass into the `announceScore` method. Additionally each assertion now gets its own test method, to keep the setup/state localised to the assertion which requires it.
Real-World Application
This is just a Kata-where’s the real world use? Katas are interesting precisely because they are small, controlled examples of situations that occur everywhere in our profession. Poorly documented and tested code can be found in most companies that develop software and all over GitHub. Next time you find yourself in this situation, save yourself time and mental energy by giving Diffblue Cover a try.
Originally published at https://www.diffblue.com on September 11, 2020.