Golden Files — Why you should use them
A Golden File is the absolute source of truth. You validate your response against the Golden File. In nutshell, the Golden file contains the response you expect from your API.
Talk is Cheap, Here’s the Code
var update = flag.Bool("update", false, "update .golden.json files")
func TestRespose(t *testing.T) {
response := DoSomething() // Make API calls
goldenFile := "ok.golden.json"
// If update is set, write to the golden file
if *update {
ioutil.WriteFile(goldenFile, response, 0644)
}
expected, err := ioutil.ReadFile(goldenFile)
if err != nil {
// Handle error
}
if !bytes.Equal(goldenFile, response) {
// Test Failure
}
}
We’ve essentially reduced the test size by more than 70%. Instead of having
multiple assert statements throughout the test, we now have a single
bytes.Equal
that validates the entire response data.
The update flag would allow us to create/update the golden files when the API response changes.
The workflow for running the tests now becomes —
- Run
go test
and make sure the test fails. - Ensure the response from your API is accurate.
- Run
go test -update
to update the existing golden file (or create new ones). - Re-run
go test
to ensure the test does not fail now.
With this simple piece of code, your tests are now much cleaner and easier to maintain.
Be smart, use Golden Files.
The Problem — ID and Timestamp
One of the major issues with using the Golden files is dealing with the ID and Timestamp. When the response from an API contains the unique ID representing the resource and its Timestamp, the golden file pattern would fail since these two entities aren’t static. The Timestamp and ID would never be same for any two entities (in normal circumstances). For instance, if you create a new resource, the timestamp would be the current time while the timestamp in the golden file would be the time when the golden file was created. This makes it very difficult to use the golden file for data with ID and Timestamp.
But, wait — There’s a way to deal with ID and Timestamps
An elegant way of dealing with ID and Timestamps is to replace all the
- ID with
0001
,0002
,0003
,....
,000N
and, Timestamps
with0001–01–01T00:00:00Z
orMon, 01 Jan 0001 00:00:00 GMT
Let’s see the code —
// Compares the given actualObj with the goldenFile
func CompareWithGolden(t *testing.T, goldenFile string, actualObj interface{}) {
expected, _ := ioutil.ReadFile(goldenFile)
expectedStr := string(expected)
actualStr := string(actualObj)
// Replace ID
expectedStr, _ = replaceIDs(expectedStr)
actualStr, _ = replaceIDs(actualStr)
// Replace Timestamp
expectedStr, _ = replaceTimes(expectedStr)
actualStr, _ = replaceTimes(actualStr)
if expectedStr != actualStr {
// Test Failed
}
}
// findIDs returns an array of unique IDs that have been found in // the given string
func findIDs(str string) ([]id, error){
pattern := "^\d{4}$"
idRegexp, err := regexp.Compile(pattern)
uniqIDs := map[id]struct{}{}
var res []id
for _, idStr := range idRegexp.FindAllString(str, -1) {
ID, _ := id.FromString(idStr)
_, alreadyInMap := uniqIDs[ID]
if !alreadyInMap {
uniqIDs[ID] = struct{}{}
// append to array
res = append(res, ID)
}
}
return res, nil
}
// replaceIDs finds all IDs in the given string and replaces them // with 0001, 0002, 0003, ...., 000N
func replaceIDs(str string) (string, error) {
replacementPattern := "%04d"
ids, err := findIDs(str)
newStr := str
for idx, id := range ids {
newStr = strings.Replace(
newStr,
id.String(),
fmt.Sprintf(replacementPattern, idx+1),
-1)
}
return newStr, nil
}
func replaceTimes(str string) (string, error) {
// Works similar to replaceIDs method
}
The IDs
and Timestamps
generated by the above code snippet would be same for
the actual
and the expected
data. Thus, your tests would no longer fail
because of IDs
or Timestamps
.
The Above code snippet was taken from — https://github.com/fabric8-services/fabric8-wit/blob/master/controller/golden_files_test.go (Thanks to the ever-awesome Konrad Kleine for writing this piece of code)
You can find more examples of Golden Files on — https://github.com/fabric8-services/fabric8-wit/tree/master/controller/test-files
More information