
Unit Testing on Legacy Code
Blog post series
This blog post is part of a series about legacy coderetreat and legacy code techniques you can apply during your work. Please click to see more sessions about legacy code.
Purpose
In the previous episode post we extracted a class. The code we just extracted is tested at this moment only with System Tests. Now it’s the time to start writing unit tests, considering that we already have some system tests in place.
Concept
We have this class, newly extracted from the tangled code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class PlayerMessage { static String createWhenNewPlayerAdded(String playerName) { return playerName + " was added"; } static String createWithNumber(int playerNumber) { return "They are player number " + playerNumber; } static String createWhenSentToPenaltyBox(String currentPlayer) { return currentPlayer + " was sent to the penalty box"; } } |
At this moment all the methods are static and not public.
Step 1: Try writing a unit test
1 2 3 4 |
@Test public void generatesCorrectPlayerAddedMessage(){ PlayerMessage. } |
We cannot call any of the methods from the class PlayerMessage, because they are not public.
Step 2: Make the methods from PlayerMessage public.
We only want to test public methods. We must therefore make the methods in PlayerMessage public, before we start testing them.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class PlayerMessage { public static String createWhenNewPlayerAdded(String playerName) { return playerName + " was added"; } public static String createWithNumber(int playerNumber) { return "They are player number " + playerNumber; } public static String createWhenSentToPenaltyBox(String currentPlayer) { return currentPlayer + " was sent to the penalty box"; } } |
Step 3: Write the first basic unit test
When we write unit tests on new features we always want to follow the cycle:
- Red (the test is written, but not the production code)
- Green (the production code is implemented and makes the test pass)
- Refactor (improve the structure of the code)
In this case we write unit tests on existing code, so we will want to see them green from the beginning. These are characterization unit tests. Characterization tests are a method of characterizing the system’s functionality with the use of automated testing.
This first test focuses only on the basic behaviour: using a correct player name, the message will be correct. In the next steps we will focus also on writing negative tests, tests that verify the system’s behaviour in exceptional solutions like having empty player name, a special alphabet, etc.
We want to start writing one unit test to be able to see how easy it is to write.
1 2 3 4 5 6 7 8 |
public class PlayerMessageTests { @Test public void generatesCorrectPlayerAddedMessage(){ String message = PlayerMessage.createWhenNewPlayerAdded("some player name"); assertEquals("some player name was added", message); } } |
Step 4: Write the rest of the basic unit tests
Because it was clear the first unit test is so easy to write, we can write all the basic happy case tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PlayerMessageTests { @Test public void generatesCorrectPlayerAddedMessage(){ String message = PlayerMessage.createWhenNewPlayerAdded("some player name"); assertEquals("some player name was added", message); } @Test public void generatesCorrectPlayerNumberMessage(){ String message = PlayerMessage.createWithNumber(2); assertEquals("They are player number 2", message); } @Test public void generatesSentToPenaltyBoxMessage(){ String message = PlayerMessage.createWhenSentToPenaltyBox("some player"); assertEquals("some player was sent to the penalty box", message); } } |
Step 5: Add unit tests for special cases
Let’s say that we have some acceptance cases: we need to support diacritics for Romanian. So for that we need to write unit tests for these letters.
1 2 3 4 5 6 7 |
@Test public void generatesCorrectPlayerAddedMessageForRomanianDiacritics(){ String playerNameWithDiacritics = "șțăâîȘȚĂÂÎ"; String message = PlayerMessage.createWhenNewPlayerAdded(playerNameWithDiacritics); assertEquals("șțăâîȘȚĂÂÎ was added", message); } |
In this way we need to add tests for the method PlayerMessage.createWhenSentToPenaltyBox as well.
Step 6: Add unit tests to document possible bugs
Often in this stage we find cases where the system behaves in a possibly incorrect way. It is highly possible to find bugs. But we should not change the production code. We should just document the cases in a way, and then discuss with the product people about each case.
Here is one case that seems wrong: the system can create a message when the player name is empty:
1 2 3 4 5 6 7 8 |
@Test @PossibleBug public void generatesPlayerMessageWhenPlayerNameIsEmpty(){ String emptyPlayerName = ""; String message = PlayerMessage.createWhenNewPlayerAdded(emptyPlayerName); assertEquals(" was added", message); } |
I have created a special annotation called PossibleBug that I use to document the cases I need to talk with the product people. In this way I create a backlog of discussion, and I do not need to focus now on creating the list of possible issues. I can do that automatically afterwards.
In this case we can write the same type of tests for PlayerMessage.createWhenSentToPenaltyBox, in the case the player name is empty. The same for PlayerMessage.createWithNumber when the player number is zero or a negative number. All of these situations can generate possible bugs.
Step 7: Test that the production system still works
You can do this with the batch of automated tests you already have (system, component, unit, etc), but you should also do some manual testing.
Outcomes
We started with a class having non-public methods. The purpose of this session was to start covering the class with tests. In this moment we have short and clear unit tests, written in isolation of any slow dependencies. These tests are fast and we need to run then any time we will change the production code, to make sure we do not introduce defects.
In the same way as shown in the last blog posts, we can extract pure functions, extract classes and then cover these classes with unit tests.
Remarks
The classes are a lot easier to be unit tested if we extract pure functions from the initial class. Pure functions are usually simple, clear and short. This is why we can test them very fast and go on extracting the next class. Focusing on extracting small methods is important because it enables a good flow of extraction -> refactoring -> testing -> refactoring.
When writing the unit tests we always need to think how much we need to test. Some good hints to decide the tests we need to write are risk based testing, equivalence partitioning and behaviour slicing.
History
Unit testing started to exist some tens of years after the computers were invented. But it got more and more used after Kent Beck published his book Extreme Programming Explained (1999), where Unit Testing is considered one of the core practices of Extreme Programming (XP).
Code Cast
Please find here a code cast in Java about this session
Acknowledgements
Many thanks to Thomas Sundberg for proofreading this post.
This 3-day advanced workshop is aimed to improve the knowledge of experienced programmers on how to evolve a software system. It is a practical workshop, with coding exercises and discussions around design options.
Evolutionary Design is the practice of growing a system in a natural way, by adding the minimum amount of code to satisfy the business needs in an iterative and incremental approach. When done right, the code structure changes continuously to optimize for change, thus allowing a constant speed of development for longer periods of time.
Recent Comments