Trunk Based Development and Legacy Code (Golden Master Testing)
To me, legacy code is simply code without tests. I’ve gotten some grief for this definition. What do tests have to do with whether code is bad? To me, the answer is straightforward, and it is a point that I elaborate throughout the book:
Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse.
Working effectively with legacy code
Ok, but what if I have to add new features to legacy code and I don’t have anyone to talk with, and I don’t understand the code?
The next option should be to experiment with the code so that I can learn how it works and what is doing.
To experiment with the code without introducing bugs, we have a solution, and they are called tests.
But there are no tests in Legacy Code, and it is usually hard to introduce them in a code not thought to be tested. To avoid this we could think, let’s refactor, but then we start again, to change safely code we need tests.
This seems “which came first: the chicken or the egg?” problem, what to do then?.
Golden Master technique
Golden master is a snapshot testing technique with a lot of samples.
We will accept that the current code is right, so we are not going to discuss the current behavior.
The idea of golden master is to run our program with different inputs and to save the results of the program into files, we will identify each file based on the inputs, this is golden master (the current state).
Once we have these files created (our golden master) we will create a simple test that execute our program with the same arguments we used in our golden master and will generate strings that can be compared with the content of our saved files.
If both results are the same per each set of inputs then the test passes, if they are different then the test will fail, explaining the file that failed and the differences with that file.
Let’s use the ugly trivia kata to show the technique, let’s say that we have a game that paints in the output the resultant of a random game played by some players.
package com.adaptionsoft.games.trivia.runner;
import java.util.Random;
import com.adaptionsoft.games.uglytrivia.Game;
public class GameRunner {
private static boolean notAWinner;
public static void main(String[] args) {
Game aGame = new Game();
aGame.add("Chet");
aGame.add("Pat");
aGame.add("Sue");
Random rand = new Random();
do {
aGame.roll(rand.nextInt(5) + 1);
if (rand.nextInt(9) == 7) {
notAWinner = aGame.wrongAnswer();
} else {
notAWinner = aGame.wasCorrectlyAnswered();
}
} while (notAWinner);
}
}
We are not very interested in the code but in the technique itself, so to apply golden master easily in this code we have two problems. The first one is that the runner is inside main, hard to write tests with a main method. Also, the code it is using a random number to call the roll method in the aGame object.
To apply the golden master technique, we are going to do a small refactor to allow us to use golden master. It needs to be a very small and very limited refactor. We don’t want to introduce bugs, so let’s try to change the minimum code in a safer way to avoid it.
package com.adaptionsoft.games.trivia.runner;
import java.util.Random;
import com.adaptionsoft.games.uglytrivia.Game;
public class GameRunner {
private static boolean notAWinner;
public static void main(String[] args) {
runTheGame(new Random());
}
public static void runTheGame(Random random) {
Game aGame = new Game();
aGame.add("Chet");
aGame.add("Pat");
aGame.add("Sue");
Random rand = random;
do {
aGame.roll(rand.nextInt(5) + 1);
if (rand.nextInt(9) == 7) {
notAWinner = aGame.wrongAnswer();
} else {
notAWinner = aGame.wasCorrectlyAnswered();
}
} while (notAWinner);
}
}
We moved the logic inside the runTheGame method, and we passed the random number as a parameter inside the static method. The random is passed as an argument to make this code deterministic.
If we create a random with a seed, we will have the same set of games played once and again, so we will be able to compare the results with golden master.
This is an example of the golden master test that we could apply to that code:
package com.adaptionsoft.games.trivia.runner;
import org.junit.Test;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.assertEquals;
public class GameRunnerGoldenMasterTest {
private static List<String> readContent(File file) throws IOException {
FileInputStream fileInputStream = new FileInputStream(file);
return readStream(fileInputStream);
}
private static List<String> readStream(InputStream fileInputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
List<String> result = new ArrayList<String>();
while (reader.ready()) {
result.add(reader.readLine());
}
fileInputStream.close();
return result;
}
@Test
public void goldenMaster() throws IOException {
for (int i = 0; i < 1000; i++) {
ByteArrayOutputStream inMemoryStream = new ByteArrayOutputStream();
PrintStream goldenMasterFile = new PrintStream(inMemoryStream);
System.setOut(goldenMasterFile);
Random random = new Random(i);
GameRunner.runTheGame(random);
File file = new File("./goldenFile" + i + ".txt");
assertEquals("Execution failed for iteration " + i, readStream(new ByteArrayInputStream(inMemoryStream.toByteArray())), readContent(file));
}
}
public static void main(String[] args) throws FileNotFoundException {
for (int i = 0; i < 1000; i++) {
PrintStream goldenMasterFile = new PrintStream(new FileOutputStream("./goldenFile" + i + ".txt"));
System.setOut(goldenMasterFile);
Random random = new Random(i);
GameRunner.runTheGame(random);
goldenMasterFile.close();
}
}
}
The main method here is the one that created golden master, it runs the game 1000 times with different seeds, and we save all of them in a goldenFile{i} per execution.
We will execute this main at the beginning of our refactoring, but also if we decide that a change to the output is a valid change. In that case, we have to recreate our golden master.
Our test runs the game against the same seed and compares the result with the file, if there is any difference the assertion will fail.
The number of iterations is a compromise between how fast the test is and how many different inputs we test, so we will be more or less secure if we have introduced a bug or not. You cannot wait for a test 10mins or having to manage 100000 files.
So we have created a kind of test that can help us with our confidence of not introducing bugs. The cost of creating this test is relatively small and can help us to start our refactoring process to gain knowledge about our code.
This is not a great test, it is not telling us that there is a bug, it is just telling us that the output changed, this could be because of a good reason or because of a bug, let’s take that into account, this is why the outputs are split to check them and see what was changed.
Once we have created our golden master and our test to check against it, we can experiment with refactors in the code to extract parts and create tests to that parts, so that we can add new features to that code iteratively in small increments.
We can apply Golden Master to do Trunk Based Development in Legacy code to refactor, improve the design, add new tests, to remove the golden master test when we feel that we can stop calling that code Legacy Code.
Subscribe to my newsletter
Read articles from Javier Lopez directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by