Unit Testing: Golden Files
Jan 8, 2019
4 minutes read

I mentioned earlier the unit test pattern used heavily in the go circles, and that is table-driven tests. Another pattern, though less common, used in the go circles is the golden file pattern. It is less common only because it has less use-cases.

Imagine that you have a unit test that checks that the response from an API matches a particular JSON:

func TestAPIGet(t testing.T*) {
	tests := []struct{
		id   int,
		want string,
	}{
		{ 11, '{"error": "not found"}'},
		{ 10, '{"user_id" : 10, "age" : 50}'},
	}

	for _, test := range tests {
		api := NewAPI();
		got := api.Get(test.id);
		if (got != test.want) {
			t.Fatalf("got %v, but wanted %v", got, test.want);
		}
	}
}

There is nothing new here: a simple use of the table-driven design.

One thing stands out: embedding the json inside the test object. It is readable, you might say, and it is. I can clearly see what the test is doing by a quick scan. However, there are two problems that rise in this example:

  1. When the json becomes larger (could be due to returning many many fields), reading the json inside the test object becomes a problem. Tester can no longer understand what the json contain. We need a way to separate the code that tests, from the data that it tests against.
  2. When we need to update the json, maybe due to adding a new field to the response, it is very difficult to update all the json values inside the test object. We need a way to update the response easily.

Golden file pattern is used to solve this issue. The idea is to put the json string inside a file, and pass the file name to the test:

func readFile(filepath string) string {
	//... read the file and return the content of the file as string
}

func TestAPIGet(t testing.T*) {
	tests := []struct{
		id         int,
		goldenFile string,
	}{
		{ 11, "not_found_response.json"},
		{ 10, "user_10_response.json"},
	}

	for _, test := range tests {
		api := NewAPI();
		got := api.Get(test.id);
		want := readFile(test.goldenFile)
		if (got != want) {
			t.Fatalf("got %v, but wanted %v", got, want);
		}
	}
}

This solves (1). Now the reader does not need to parse json string inside the code to see what the expected response should be. Rather, they can just open a golden file (the file that contains the expected response) in their favorite editor to read the json. The function readFile read the file and returns the content. Now the test is clean! I generally like to put my golden files inside a folder with the name test-fixtures, and so I use readFile to append that folder name to the file path.

This marginally solves the second problem, however. We still would have to spend quite sometime updating all the files when the json payload changes. To solve this, we introduce an update flag so that when we run our tests with this update flag, instead of comparing the response to the file content, we automatically override the file with the response:

var (
	update = flag.Bool("update", false, "update golden files")
)

// readFile reads and return the content of a given file
func readFile(filepath string) string {
}

// writeFile writes the content to the given file
func writeFile(filepath, content string) {
}

func TestAPIGet(t testing.T*) {
	// parse the custom flags
	flag.Parse()

	tests := []struct{
		id         int,
		goldenFile string,
	}{
		{ 11, "not_found_response.json"},
		{ 10, "user_10_response.json"},
	}

	for _, test := range tests {
		api := NewAPI();
		got := api.Get(test.id);
		if *update {
			writeFile(test.goldenFile, got);
		}
		want := readFile(test.goldenFile)
		if (got != want) {
			t.Fatalf("got %v, but wanted %v", got, want);
		}
	}
}

With this, you can pass the -update flag inside your test, and that will override your golden files with the response. This is of course dangerous because your tests will be overridden and will not be recoverable (unless you have a tool like git history), and hence you should use it only after inspecting the result.

Golden files can be used for binary content or strings, or anything not easy to read. I use it often for checking the REST API responses.


Back to posts