
Anyone who works in the software development world will have had the need to add features on the legacy code, perhaps inherited from the previous team, and on which you need to make an urgent fix.
Many definitions of legacy code can be found in literature, the one that I prefer is: “By Legacy code, we mean the profitable code that we feel afraid to change”. This definition contains two fundamental concepts:
- The code has to be profitable. If it doesn’t, we have no interest in changing it.
- It must breed fear of modifying it because we can introduce new bug o break thing with shadow dependencies.
The ease of making mistakes increases when:
- The code is not covered by tests.
- The code is not clean; the single-responsibility principle is not respected.
- The code is badly designed, or becomes ill-structured over time: making a change to a piece of code can have some side effects.
- You haven’t the time to get thorough knowledge of what you are modifying.
A powerful weapon that we have available as developers is the tests. The tests give us security on the result and are a way to detect errors quickly. But how can we test code which we don’t know? Building a Unit Test suite would provide us an in-depth knowledge of the project, but it would keep long time with high cost. If we cannot test details, we can use Characterization Test, which is tests that describe the behavior of a piece of software.
A pattern that plays an important role in this situation is Golden Master Pattern. The basic idea is straightforward: if we can’t go deep in detail, we need some indicators of the entire execution. We catch the output (stdout, images, log files, etc.) of a correct execution, and this is our Golden Master, that we can use for expected output. If the output of current execution matches, we can be confident that our change hasn’t introduced new errors.
To show the use of the Golden Master Pattern, let’s start with an example (the complete code can be found here). Our company develops games for command line, including the Tic Tac Toe Game (the implementation is taken from here), our boss asks us to change the game to give the ability of resize game board. Let’s take a look at the code:
namespace Tris
{
public class Game
{
//making array and
//by default I am providing 0-9 where no use of zero
static char[] arr = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static int player = 1; //By default player 1 is set
static int choice; //This holds the choice at which position user want to mark
// The flag variable checks who has won if its value is 1 then someone has won the match if -1 then Match has Draw if 0 then match is still running
static int flag = 0;
public static void run()
{
do
{
Console.Clear();// whenever loop will be again start then screen will be clear
Console.WriteLine("Player1:X and Player2:O");
Console.WriteLine("\n");
if (player % 2 == 0)//checking the chance of the player
{
Console.WriteLine("Player 2 Chance");
}
else
{
Console.WriteLine("Player 1 Chance");
}
Console.WriteLine("\n");
Board();// calling the board Function
choice = int.Parse(Console.ReadLine());//Taking users choice
// checking that position where user want to run is marked (with X or O) or not
if (arr[choice] != 'X' && arr[choice] != 'O')
{
if (player % 2 == 0) //if chance is of player 2 then mark O else mark X
{
arr[choice] = 'O';
player++;
}
else
{
arr[choice] = 'X';
player++;
}
}
else //If there is any position where user wants to run and that is already marked then show message and load board again
{
Console.WriteLine("Sorry the row {0} is already marked with {1}", choice, arr[choice]);
Console.WriteLine("\n");
Console.WriteLine("Please wait 2 second board is loading again.....");
Thread.Sleep(2000);
}
flag = CheckWin();// calling of check win
} while (flag != 1 && flag != -1);// This loof will be run until all cell of the grid is not marked with X and O or some player is not winner
Console.Clear();// clearing the console
Board();// getting filled board again
if (flag == 1)// if flag value is 1 then someone has win or means who played marked last time which has win
{
Console.WriteLine("Player {0} has won", (player % 2) + 1);
}
else// if flag value is -1 the match will be drawn and no one is the winner
{
Console.WriteLine("Draw");
}
Console.ReadLine();
}
// Board method which creats board
private static void Board()
{
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[1], arr[2], arr[3]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[4], arr[5], arr[6]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[7], arr[8], arr[9]);
Console.WriteLine(" | | ");
}
private static int CheckWin()
{
#region Horzontal Winning Condtion
//Winning Condition For First Row
if (arr[1] == arr[2] && arr[2] == arr[3])
{
return 1;
}
//Winning Condition For Second Row
else if (arr[4] == arr[5] && arr[5] == arr[6])
{
return 1;
}
//Winning Condition For Third Row
else if (arr[6] == arr[7] && arr[7] == arr[8])
{
return 1;
}
#endregion
#region vertical Winning Condtion
//Winning Condition For First Column
else if (arr[1] == arr[4] && arr[4] == arr[7])
{
return 1;
}
//Winning Condition For Second Column
else if (arr[2] == arr[5] && arr[5] == arr[8])
{
return 1;
}
//Winning Condition For Third Column
else if (arr[3] == arr[6] && arr[6] == arr[9])
{
return 1;
}
#endregion
#region Diagonal Winning Condition
else if (arr[1] == arr[5] && arr[5] == arr[9])
{
return 1;
}
else if (arr[3] == arr[5] && arr[5] == arr[7])
{
return 1;
}
#endregion
#region Checking For Draw
// If all the cells or values filled with X or O then any player has won the match
else if (arr[1] != '1' && arr[2] != '2' && arr[3] != '3' && arr[4] != '4' && arr[5] != '5' && arr[6] != '6' && arr[7] != '7' && arr[8] != '8' && arr[9] != '9')
{
return -1;
}
#endregion
else
{
return 0;
}
}
}
}
At a quick reading, the code appears confused, the responsibility is not correctly separated, and the variable names are not meaningful. After an accurate reading, we are able to found the game board, which was stored in ‘static char[] arr’. Adding new elements to the array has no effects because the array accesses directly in PrintBoard and CheckWin functions. Now we know that to resize a game board, a large part of the code must be changed.
Create a new project and run game:
class Program
{
static void Main(string[] args)
{
Game.run();
}
}
As soon as we printed the board, the game asks for user input. We can automate by reading the input from file.
class Program
{
private const string InputPath = "input.txt";
public static void Main(string[] args)
{
var input = new StreamReader(new FileStream(InputPath, FileMode.Open));
Console.SetIn(input);
Game.run();
input.Close();
}
}
The set of all input is too large to use brute-force testing. What we can do is sampling the input. To do that, we consider the possible final scores of a tic tac toe game:
- Player 1 Wins
- Player 2 Wins
- Draw
Select the minimum set of test that cover the three cases, writing the paths in text files and collect results in the golendenMaster folder:
class Program
{
private const string InputFolderPath = "input/";
private const string OutputFolderPath = "goldenMaster/";
public static void Main(string[] args)
{
int i = 1;
foreach (var filePath in Directory.GetFiles(InputFolderPath)) {
var input = new StreamReader(new FileStream(filePath, FileMode.Open));
var output = new StreamWriter(new FileStream(OutputFolderPath + "output" + i.ToString() + ".txt" , FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
i++;
}
}
}
The three results files represent our Golden Master, basing on which we can build some Characterization Tests:
[Test]
public void WinPlayerOne()
{
inputPath = InputFolderPath + "input1.txt";
outputPath = OutputFolderPath + "output.txt";
var goldenMasterOutput = GoldenMasterOutput + "output1.txt";
var input = new StreamReader(new FileStream(inputPath, FileMode.Open));
var output = new StreamWriter(new FileStream(outputPath, FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
Assert.True(AreFileEquals(goldenMasterOutput, outputPath));
}
private bool AreFileEquals(string expectedPath, string actualPath)
{
byte[] bytes1 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(expectedPath)));
byte[] bytes2 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(actualPath)));
return bytes1.SequenceEqual(bytes2);
}
As long as the tests are green, we can refactor without fear of breaking something. One possible result could be the following:
public static void run()
{
char[] board = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
int actualPlayer = 1;
while (CheckWin(board) == 0)
{
PrintPlayerChoise(actualPlayer);
PrintBoard(board);
var choice = ReadPlayerChoise();
if (isBoardCellAlreadyTaken(board[choice]))
{
PrintCellIsAlreadyMarketMessage(board[choice], choice);
continue;
}
board[choice] = GetPlayerMarker(actualPlayer);
actualPlayer = UpdatePlayer(actualPlayer);
}
PrintResult(board, actualPlayer);
}
From this block of code come to light the concept of Board and his responsibility. Let’s try to extract behavior in the new Board class. The new Board should be able to:
- Print board
- Mark the player choice
- Check if there is a winner
Using TDD (for more details, read the article by Adolfo) to develop a resizable Board ( find the complete code of test here and the one of class here). Now try to insert them in the game, and check if the Golden Master remains green:
private const int Boardsize = 3;
public static void run()
{
Board board = new Board(Boardsize);
int actualPlayer = 1;
while (board.CheckWin() == -1)
{
PrintPlayerChoise(actualPlayer);
Console.WriteLine(board.Print());
var choice = ReadPlayerChoise();
if (!board.UpdateBoard(actualPlayer, choice))
{
PrintCellIsAlreadyMarketMessage(board.GetCellValue(choice), choice);
continue;
}
actualPlayer = UpdatePlayer(actualPlayer);
}
PrintResult(board, actualPlayer);
}
At this point we can restore the stdin/stdout and read the dimensions of board from user:
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Insert Diagonal dimension of Board: ");
var boardSize = int.Parse(Console.ReadLine());
Game.run(boardSize);
}
}
As you have seen, thanks to Golden Master Pattern, we are able to dominate legacy code and refactor without fear. But all that glitters is not gold: using the Golden Master can be hard in case of “noise output”, which is the output useless for execution, but that changes over time (es. timestamp, thread name, etc. ). In this case, we can filter output and consider only the significant part.
I hope it will be useful for you the next time you inherit a legacy project: after all, we fear what we cannot control only!
See you at the next article